Ver Fonte

refactor: disable telemetry backends

paoloanzn há 2 meses atrás
pai
commit
7a558e9640

+ 3 - 131
src/entrypoints/init.ts

@@ -1,11 +1,8 @@
 import { profileCheckpoint } from '../utils/startupProfiler.js'
 import '../bootstrap/state.js'
 import '../utils/config.js'
-import type { Attributes, MetricOptions } from '@opentelemetry/api'
 import memoize from 'lodash-es/memoize.js'
 import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
-import type { AttributedCounter } from '../bootstrap/state.js'
-import { getSessionCounter, setMeter } from '../bootstrap/state.js'
 import { shutdownLspServerManager } from '../services/lsp/manager.js'
 import { populateOAuthAccountInfoIfNeeded } from '../services/oauth/client.js'
 import {
@@ -15,7 +12,6 @@ import {
 import {
   initializeRemoteManagedSettingsLoadingPromise,
   isEligibleForRemoteManagedSettings,
-  waitForRemoteManagedSettingsToLoad,
 } from '../services/remoteManagedSettings/index.js'
 import { preconnectAnthropicApi } from '../utils/apiPreconnect.js'
 import { applyExtraCACertsFromConfig } from '../utils/caCertsConfig.js'
@@ -26,14 +22,13 @@ import { detectCurrentRepository } from '../utils/detectRepository.js'
 import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
 import { initJetBrainsDetection } from '../utils/envDynamic.js'
 import { isEnvTruthy } from '../utils/envUtils.js'
-import { ConfigParseError, errorMessage } from '../utils/errors.js'
+import { ConfigParseError } from '../utils/errors.js'
 // showInvalidConfigDialog is dynamically imported in the error path to avoid loading React at init
 import {
   gracefulShutdownSync,
   setupGracefulShutdown,
 } from '../utils/gracefulShutdown.js'
 import {
-  applyConfigEnvironmentVariables,
   applySafeConfigEnvironmentVariables,
 } from '../utils/managedEnv.js'
 import { configureGlobalMTLS } from '../utils/mtls.js'
@@ -41,19 +36,9 @@ import {
   ensureScratchpadDir,
   isScratchpadEnabled,
 } from '../utils/permissions/filesystem.js'
-// initializeTelemetry is loaded lazily via import() in setMeterState() to defer
-// ~400KB of OpenTelemetry + protobuf modules until telemetry is actually initialized.
-// gRPC exporters (~700KB via @grpc/grpc-js) are further lazy-loaded within instrumentation.ts.
 import { configureGlobalAgents } from '../utils/proxy.js'
-import { isBetaTracingEnabled } from '../utils/telemetry/betaSessionTracing.js'
-import { getTelemetryAttributes } from '../utils/telemetryAttributes.js'
 import { setShellIfWindows } from '../utils/windowsPaths.js'
 
-// initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources
-
-// Track if telemetry has been initialized to prevent double initialization
-let telemetryInitialized = false
-
 export const init = memoize(async (): Promise<void> => {
   const initStartTime = Date.now()
   logForDiagnosticsNoPII('info', 'init_started')
@@ -87,24 +72,6 @@ export const init = memoize(async (): Promise<void> => {
     setupGracefulShutdown()
     profileCheckpoint('init_after_graceful_shutdown')
 
-    // Initialize 1P event logging (no security concerns, but deferred to avoid
-    // loading OpenTelemetry sdk-logs at startup). growthbook.js is already in
-    // the module cache by this point (firstPartyEventLogger imports it), so the
-    // second dynamic import adds no load cost.
-    void Promise.all([
-      import('../services/analytics/firstPartyEventLogger.js'),
-      import('../services/analytics/growthbook.js'),
-    ]).then(([fp, gb]) => {
-      fp.initialize1PEventLogging()
-      // Rebuild the logger provider if tengu_1p_event_batch_config changes
-      // mid-session. Change detection (isEqual) is inside the handler so
-      // unchanged refreshes are no-ops.
-      gb.onGrowthBookRefresh(() => {
-        void fp.reinitialize1PEventLoggingIfConfigChanged()
-      })
-    })
-    profileCheckpoint('init_after_1p_event_logging')
-
     // Populate OAuth account info if it is not already cached in config. This is needed since the
     // OAuth account info may not be populated when logging in through the VSCode extension.
     void populateOAuthAccountInfoIfNeeded()
@@ -238,103 +205,8 @@ export const init = memoize(async (): Promise<void> => {
 })
 
 /**
- * Initialize telemetry after trust has been granted.
- * For remote-settings-eligible users, waits for settings to load (non-blocking),
- * then re-applies env vars (to include remote settings) before initializing telemetry.
- * For non-eligible users, initializes telemetry immediately.
- * This should only be called once, after the trust dialog has been accepted.
+ * The OSS build does not initialize product telemetry after trust.
  */
 export function initializeTelemetryAfterTrust(): void {
-  if (isEligibleForRemoteManagedSettings()) {
-    // For SDK/headless mode with beta tracing, initialize eagerly first
-    // to ensure the tracer is ready before the first query runs.
-    // The async path below will still run but doInitializeTelemetry() guards against double init.
-    if (getIsNonInteractiveSession() && isBetaTracingEnabled()) {
-      void doInitializeTelemetry().catch(error => {
-        logForDebugging(
-          `[3P telemetry] Eager telemetry init failed (beta tracing): ${errorMessage(error)}`,
-          { level: 'error' },
-        )
-      })
-    }
-    logForDebugging(
-      '[3P telemetry] Waiting for remote managed settings before telemetry init',
-    )
-    void waitForRemoteManagedSettingsToLoad()
-      .then(async () => {
-        logForDebugging(
-          '[3P telemetry] Remote managed settings loaded, initializing telemetry',
-        )
-        // Re-apply env vars to pick up remote settings before initializing telemetry.
-        applyConfigEnvironmentVariables()
-        await doInitializeTelemetry()
-      })
-      .catch(error => {
-        logForDebugging(
-          `[3P telemetry] Telemetry init failed (remote settings path): ${errorMessage(error)}`,
-          { level: 'error' },
-        )
-      })
-  } else {
-    void doInitializeTelemetry().catch(error => {
-      logForDebugging(
-        `[3P telemetry] Telemetry init failed: ${errorMessage(error)}`,
-        { level: 'error' },
-      )
-    })
-  }
-}
-
-async function doInitializeTelemetry(): Promise<void> {
-  if (telemetryInitialized) {
-    // Already initialized, nothing to do
-    return
-  }
-
-  // Set flag before init to prevent double initialization
-  telemetryInitialized = true
-  try {
-    await setMeterState()
-  } catch (error) {
-    // Reset flag on failure so subsequent calls can retry
-    telemetryInitialized = false
-    throw error
-  }
-}
-
-async function setMeterState(): Promise<void> {
-  // Lazy-load instrumentation to defer ~400KB of OpenTelemetry + protobuf
-  const { initializeTelemetry } = await import(
-    '../utils/telemetry/instrumentation.js'
-  )
-  // Initialize customer OTLP telemetry (metrics, logs, traces)
-  const meter = await initializeTelemetry()
-  if (meter) {
-    // Create factory function for attributed counters
-    const createAttributedCounter = (
-      name: string,
-      options: MetricOptions,
-    ): AttributedCounter => {
-      const counter = meter?.createCounter(name, options)
-
-      return {
-        add(value: number, additionalAttributes: Attributes = {}) {
-          // Always fetch fresh telemetry attributes to ensure they're up to date
-          const currentAttributes = getTelemetryAttributes()
-          const mergedAttributes = {
-            ...currentAttributes,
-            ...additionalAttributes,
-          }
-          counter?.add(value, mergedAttributes)
-        },
-      }
-    }
-
-    setMeter(meter, createAttributedCounter)
-
-    // Increment session counter here because the startup telemetry path
-    // runs before this async initialization completes, so the counter
-    // would be null there.
-    getSessionCounter()?.add(1)
-  }
+  return
 }

+ 7 - 302
src/services/analytics/datadog.ts

@@ -1,307 +1,12 @@
-import axios from 'axios'
-import { createHash } from 'crypto'
-import memoize from 'lodash-es/memoize.js'
-import { getOrCreateUserID } from '../../utils/config.js'
-import { logError } from '../../utils/log.js'
-import { getCanonicalName } from '../../utils/model/model.js'
-import { getAPIProvider } from '../../utils/model/providers.js'
-import { MODEL_COSTS } from '../../utils/modelCost.js'
-import { isAnalyticsDisabled } from './config.js'
-import { getEventMetadata } from './metadata.js'
-
-const DATADOG_LOGS_ENDPOINT =
-  'https://http-intake.logs.us5.datadoghq.com/api/v2/logs'
-const DATADOG_CLIENT_TOKEN = 'pubbbf48e6d78dae54bceaa4acf463299bf'
-const DEFAULT_FLUSH_INTERVAL_MS = 15000
-const MAX_BATCH_SIZE = 100
-const NETWORK_TIMEOUT_MS = 5000
-
-const DATADOG_ALLOWED_EVENTS = new Set([
-  'chrome_bridge_connection_succeeded',
-  'chrome_bridge_connection_failed',
-  'chrome_bridge_disconnected',
-  'chrome_bridge_tool_call_completed',
-  'chrome_bridge_tool_call_error',
-  'chrome_bridge_tool_call_started',
-  'chrome_bridge_tool_call_timeout',
-  'tengu_api_error',
-  'tengu_api_success',
-  'tengu_brief_mode_enabled',
-  'tengu_brief_mode_toggled',
-  'tengu_brief_send',
-  'tengu_cancel',
-  'tengu_compact_failed',
-  'tengu_exit',
-  'tengu_flicker',
-  'tengu_init',
-  'tengu_model_fallback_triggered',
-  'tengu_oauth_error',
-  'tengu_oauth_success',
-  'tengu_oauth_token_refresh_failure',
-  'tengu_oauth_token_refresh_success',
-  'tengu_oauth_token_refresh_lock_acquiring',
-  'tengu_oauth_token_refresh_lock_acquired',
-  'tengu_oauth_token_refresh_starting',
-  'tengu_oauth_token_refresh_completed',
-  'tengu_oauth_token_refresh_lock_releasing',
-  'tengu_oauth_token_refresh_lock_released',
-  'tengu_query_error',
-  'tengu_session_file_read',
-  'tengu_started',
-  'tengu_tool_use_error',
-  'tengu_tool_use_granted_in_prompt_permanent',
-  'tengu_tool_use_granted_in_prompt_temporary',
-  'tengu_tool_use_rejected_in_prompt',
-  'tengu_tool_use_success',
-  'tengu_uncaught_exception',
-  'tengu_unhandled_rejection',
-  'tengu_voice_recording_started',
-  'tengu_voice_toggled',
-  'tengu_team_mem_sync_pull',
-  'tengu_team_mem_sync_push',
-  'tengu_team_mem_sync_started',
-  'tengu_team_mem_entries_capped',
-])
-
-const TAG_FIELDS = [
-  'arch',
-  'clientType',
-  'errorType',
-  'http_status_range',
-  'http_status',
-  'kairosActive',
-  'model',
-  'platform',
-  'provider',
-  'skillMode',
-  'subscriptionType',
-  'toolName',
-  'userBucket',
-  'userType',
-  'version',
-  'versionBase',
-]
-
-function camelToSnakeCase(str: string): string {
-  return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
-}
-
-type DatadogLog = {
-  ddsource: string
-  ddtags: string
-  message: string
-  service: string
-  hostname: string
-  [key: string]: unknown
-}
-
-let logBatch: DatadogLog[] = []
-let flushTimer: NodeJS.Timeout | null = null
-let datadogInitialized: boolean | null = null
-
-async function flushLogs(): Promise<void> {
-  if (logBatch.length === 0) return
-
-  const logsToSend = logBatch
-  logBatch = []
-
-  try {
-    await axios.post(DATADOG_LOGS_ENDPOINT, logsToSend, {
-      headers: {
-        'Content-Type': 'application/json',
-        'DD-API-KEY': DATADOG_CLIENT_TOKEN,
-      },
-      timeout: NETWORK_TIMEOUT_MS,
-    })
-  } catch (error) {
-    logError(error)
-  }
-}
-
-function scheduleFlush(): void {
-  if (flushTimer) return
-
-  flushTimer = setTimeout(() => {
-    flushTimer = null
-    void flushLogs()
-  }, getFlushIntervalMs()).unref()
-}
-
-export const initializeDatadog = memoize(async (): Promise<boolean> => {
-  if (isAnalyticsDisabled()) {
-    datadogInitialized = false
-    return false
-  }
-
-  try {
-    datadogInitialized = true
-    return true
-  } catch (error) {
-    logError(error)
-    datadogInitialized = false
-    return false
-  }
-})
-
 /**
- * Flush remaining Datadog logs and shut down.
- * Called from gracefulShutdown() before process.exit() since
- * forceExit() prevents the beforeExit handler from firing.
+ * Datadog logging is intentionally disabled in the OSS build.
  */
-export async function shutdownDatadog(): Promise<void> {
-  if (flushTimer) {
-    clearTimeout(flushTimer)
-    flushTimer = null
-  }
-  await flushLogs()
-}
-
-// NOTE: use via src/services/analytics/index.ts > logEvent
-export async function trackDatadogEvent(
-  eventName: string,
-  properties: { [key: string]: boolean | number | undefined },
-): Promise<void> {
-  if (process.env.NODE_ENV !== 'production') {
-    return
-  }
-
-  // Don't send events for 3P providers (Bedrock, Vertex, Foundry)
-  if (getAPIProvider() !== 'firstParty') {
-    return
-  }
 
-  // Fast path: use cached result if available to avoid await overhead
-  let initialized = datadogInitialized
-  if (initialized === null) {
-    initialized = await initializeDatadog()
-  }
-  if (!initialized || !DATADOG_ALLOWED_EVENTS.has(eventName)) {
-    return
-  }
+export const initializeDatadog = async (): Promise<boolean> => false
 
-  try {
-    const metadata = await getEventMetadata({
-      model: properties.model,
-      betas: properties.betas,
-    })
-    // Destructure to avoid duplicate envContext (once nested, once flattened)
-    const { envContext, ...restMetadata } = metadata
-    const allData: Record<string, unknown> = {
-      ...restMetadata,
-      ...envContext,
-      ...properties,
-      userBucket: getUserBucket(),
-    }
+export async function shutdownDatadog(): Promise<void> {}
 
-    // Normalize MCP tool names to "mcp" for cardinality reduction
-    if (
-      typeof allData.toolName === 'string' &&
-      allData.toolName.startsWith('mcp__')
-    ) {
-      allData.toolName = 'mcp'
-    }
-
-    // Normalize model names for cardinality reduction (external users only)
-    if (process.env.USER_TYPE !== 'ant' && typeof allData.model === 'string') {
-      const shortName = getCanonicalName(allData.model.replace(/\[1m]$/i, ''))
-      allData.model = shortName in MODEL_COSTS ? shortName : 'other'
-    }
-
-    // Truncate dev version to base + date (remove timestamp and sha for cardinality reduction)
-    // e.g. "2.0.53-dev.20251124.t173302.sha526cc6a" -> "2.0.53-dev.20251124"
-    if (typeof allData.version === 'string') {
-      allData.version = allData.version.replace(
-        /^(\d+\.\d+\.\d+-dev\.\d{8})\.t\d+\.sha[a-f0-9]+$/,
-        '$1',
-      )
-    }
-
-    // Transform status to http_status and http_status_range to avoid Datadog reserved field
-    if (allData.status !== undefined && allData.status !== null) {
-      const statusCode = String(allData.status)
-      allData.http_status = statusCode
-
-      // Determine status range (1xx, 2xx, 3xx, 4xx, 5xx)
-      const firstDigit = statusCode.charAt(0)
-      if (firstDigit >= '1' && firstDigit <= '5') {
-        allData.http_status_range = `${firstDigit}xx`
-      }
-
-      // Remove original status field to avoid conflict with Datadog's reserved field
-      delete allData.status
-    }
-
-    // Build ddtags with high-cardinality fields for filtering.
-    // event:<name> is prepended so the event name is searchable via the
-    // log search API — the `message` field (where eventName also lives)
-    // is a DD reserved field and is NOT queryable from dashboard widget
-    // queries or the aggregation API. See scripts/release/MONITORING.md.
-    const allDataRecord = allData
-    const tags = [
-      `event:${eventName}`,
-      ...TAG_FIELDS.filter(
-        field =>
-          allDataRecord[field] !== undefined && allDataRecord[field] !== null,
-      ).map(field => `${camelToSnakeCase(field)}:${allDataRecord[field]}`),
-    ]
-
-    const log: DatadogLog = {
-      ddsource: 'nodejs',
-      ddtags: tags.join(','),
-      message: eventName,
-      service: 'claude-code',
-      hostname: 'claude-code',
-      env: process.env.USER_TYPE,
-    }
-
-    // Add all fields as searchable attributes (not duplicated in tags)
-    for (const [key, value] of Object.entries(allData)) {
-      if (value !== undefined && value !== null) {
-        log[camelToSnakeCase(key)] = value
-      }
-    }
-
-    logBatch.push(log)
-
-    // Flush immediately if batch is full, otherwise schedule
-    if (logBatch.length >= MAX_BATCH_SIZE) {
-      if (flushTimer) {
-        clearTimeout(flushTimer)
-        flushTimer = null
-      }
-      void flushLogs()
-    } else {
-      scheduleFlush()
-    }
-  } catch (error) {
-    logError(error)
-  }
-}
-
-const NUM_USER_BUCKETS = 30
-
-/**
- * Gets a 'bucket' that the user ID falls into.
- *
- * For alerting purposes, we want to alert on the number of users impacted
- * by an issue, rather than the number of events- often a small number of users
- * can generate a large number of events (e.g. due to retries). To approximate
- * this without ruining cardinality by counting user IDs directly, we hash the user ID
- * and assign it to one of a fixed number of buckets.
- *
- * This allows us to estimate the number of unique users by counting unique buckets,
- * while preserving user privacy and reducing cardinality.
- */
-const getUserBucket = memoize((): number => {
-  const userId = getOrCreateUserID()
-  const hash = createHash('sha256').update(userId).digest('hex')
-  return parseInt(hash.slice(0, 8), 16) % NUM_USER_BUCKETS
-})
-
-function getFlushIntervalMs(): number {
-  // Allow tests to override to not block on the default flush interval.
-  return (
-    parseInt(process.env.CLAUDE_CODE_DATADOG_FLUSH_INTERVAL_MS || '', 10) ||
-    DEFAULT_FLUSH_INTERVAL_MS
-  )
-}
+export async function trackDatadogEvent(
+  _eventName: string,
+  _properties: { [key: string]: boolean | number | undefined },
+): Promise<void> {}

+ 19 - 420
src/services/analytics/firstPartyEventLogger.ts

@@ -1,237 +1,37 @@
-import type { AnyValueMap, Logger, logs } from '@opentelemetry/api-logs'
-import { resourceFromAttributes } from '@opentelemetry/resources'
-import {
-  BatchLogRecordProcessor,
-  LoggerProvider,
-} from '@opentelemetry/sdk-logs'
-import {
-  ATTR_SERVICE_NAME,
-  ATTR_SERVICE_VERSION,
-} from '@opentelemetry/semantic-conventions'
-import { randomUUID } from 'crypto'
-import { isEqual } from 'lodash-es'
-import { getOrCreateUserID } from '../../utils/config.js'
-import { logForDebugging } from '../../utils/debug.js'
-import { logError } from '../../utils/log.js'
-import { getPlatform, getWslVersion } from '../../utils/platform.js'
-import { jsonStringify } from '../../utils/slowOperations.js'
-import { profileCheckpoint } from '../../utils/startupProfiler.js'
-import { getCoreUserData } from '../../utils/user.js'
-import { isAnalyticsDisabled } from './config.js'
-import { FirstPartyEventLoggingExporter } from './firstPartyEventLoggingExporter.js'
-import type { GrowthBookUserAttributes } from './growthbook.js'
-import { getDynamicConfig_CACHED_MAY_BE_STALE } from './growthbook.js'
-import { getEventMetadata } from './metadata.js'
-import { isSinkKilled } from './sinkKillswitch.js'
-
 /**
- * Configuration for sampling individual event types.
- * Each event name maps to an object containing sample_rate (0-1).
- * Events not in the config are logged at 100% rate.
+ * First-party event logging is intentionally disabled in the OSS build.
+ *
+ * This file preserves the public API used throughout the CLI while ensuring no
+ * telemetry providers, exporters, or batching logic are initialized.
  */
+
+import type { GrowthBookUserAttributes } from './growthbook.js'
+
 export type EventSamplingConfig = {
   [eventName: string]: {
     sample_rate: number
   }
 }
 
-const EVENT_SAMPLING_CONFIG_NAME = 'tengu_event_sampling_config'
-/**
- * Get the event sampling configuration from GrowthBook.
- * Uses cached value if available, updates cache in background.
- */
 export function getEventSamplingConfig(): EventSamplingConfig {
-  return getDynamicConfig_CACHED_MAY_BE_STALE<EventSamplingConfig>(
-    EVENT_SAMPLING_CONFIG_NAME,
-    {},
-  )
+  return {}
 }
 
-/**
- * Determine if an event should be sampled based on its sample rate.
- * Returns the sample rate if sampled, null if not sampled.
- *
- * @param eventName - Name of the event to check
- * @returns The sample_rate if event should be logged, null if it should be dropped
- */
-export function shouldSampleEvent(eventName: string): number | null {
-  const config = getEventSamplingConfig()
-  const eventConfig = config[eventName]
-
-  // If no config for this event, log at 100% rate (no sampling)
-  if (!eventConfig) {
-    return null
-  }
-
-  const sampleRate = eventConfig.sample_rate
-
-  // Validate sample rate is in valid range
-  if (typeof sampleRate !== 'number' || sampleRate < 0 || sampleRate > 1) {
-    return null
-  }
-
-  // Sample rate of 1 means log everything (no need to add metadata)
-  if (sampleRate >= 1) {
-    return null
-  }
-
-  // Sample rate of 0 means drop everything
-  if (sampleRate <= 0) {
-    return 0
-  }
-
-  // Randomly decide whether to sample this event
-  return Math.random() < sampleRate ? sampleRate : 0
+export function shouldSampleEvent(_eventName: string): number | null {
+  return 0
 }
 
-const BATCH_CONFIG_NAME = 'tengu_1p_event_batch_config'
-type BatchConfig = {
-  scheduledDelayMillis?: number
-  maxExportBatchSize?: number
-  maxQueueSize?: number
-  skipAuth?: boolean
-  maxAttempts?: number
-  path?: string
-  baseUrl?: string
-}
-function getBatchConfig(): BatchConfig {
-  return getDynamicConfig_CACHED_MAY_BE_STALE<BatchConfig>(
-    BATCH_CONFIG_NAME,
-    {},
-  )
-}
+export async function shutdown1PEventLogging(): Promise<void> {}
 
-// Module-local state for event logging (not exposed globally)
-let firstPartyEventLogger: ReturnType<typeof logs.getLogger> | null = null
-let firstPartyEventLoggerProvider: LoggerProvider | null = null
-// Last batch config used to construct the provider — used by
-// reinitialize1PEventLoggingIfConfigChanged to decide whether a rebuild is
-// needed when GrowthBook refreshes.
-let lastBatchConfig: BatchConfig | null = null
-/**
- * Flush and shutdown the 1P event logger.
- * This should be called as the final step before process exit to ensure
- * all events (including late ones from API responses) are exported.
- */
-export async function shutdown1PEventLogging(): Promise<void> {
-  if (!firstPartyEventLoggerProvider) {
-    return
-  }
-  try {
-    await firstPartyEventLoggerProvider.shutdown()
-    if (process.env.USER_TYPE === 'ant') {
-      logForDebugging('1P event logging: final shutdown complete')
-    }
-  } catch {
-    // Ignore shutdown errors
-  }
-}
-
-/**
- * Check if 1P event logging is enabled.
- * Respects the same opt-outs as other analytics sinks:
- * - Test environment
- * - Third-party cloud providers (Bedrock/Vertex)
- * - Global telemetry opt-outs
- * - Non-essential traffic disabled
- *
- * Note: Unlike BigQuery metrics, event logging does NOT check organization-level
- * metrics opt-out via API. It follows the same pattern as Statsig event logging.
- */
 export function is1PEventLoggingEnabled(): boolean {
-  // Respect standard analytics opt-outs
-  return !isAnalyticsDisabled()
+  return false
 }
 
-/**
- * Log a 1st-party event for internal analytics (async version).
- * Events are batched and exported to /api/event_logging/batch
- *
- * This enriches the event with core metadata (model, session, env context, etc.)
- * at log time, similar to logEventToStatsig.
- *
- * @param eventName - Name of the event (e.g., 'tengu_api_query')
- * @param metadata - Additional metadata for the event (intentionally no strings, to avoid accidentally logging code/filepaths)
- */
-async function logEventTo1PAsync(
-  firstPartyEventLogger: Logger,
-  eventName: string,
-  metadata: Record<string, number | boolean | undefined> = {},
-): Promise<void> {
-  try {
-    // Enrich with core metadata at log time (similar to Statsig pattern)
-    const coreMetadata = await getEventMetadata({
-      model: metadata.model,
-      betas: metadata.betas,
-    })
-
-    // Build attributes - OTel supports nested objects natively via AnyValueMap
-    // Cast through unknown since our nested objects are structurally compatible
-    // with AnyValue but TS doesn't recognize it due to missing index signatures
-    const attributes = {
-      event_name: eventName,
-      event_id: randomUUID(),
-      // Pass objects directly - no JSON serialization needed
-      core_metadata: coreMetadata,
-      user_metadata: getCoreUserData(true),
-      event_metadata: metadata,
-    } as unknown as AnyValueMap
-
-    // Add user_id if available
-    const userId = getOrCreateUserID()
-    if (userId) {
-      attributes.user_id = userId
-    }
-
-    // Debug logging when debug mode is enabled
-    if (process.env.USER_TYPE === 'ant') {
-      logForDebugging(
-        `[ANT-ONLY] 1P event: ${eventName} ${jsonStringify(metadata, null, 0)}`,
-      )
-    }
-
-    // Emit log record
-    firstPartyEventLogger.emit({
-      body: eventName,
-      attributes,
-    })
-  } catch (e) {
-    if (process.env.NODE_ENV === 'development') {
-      throw e
-    }
-    if (process.env.USER_TYPE === 'ant') {
-      logError(e as Error)
-    }
-    // swallow
-  }
-}
-
-/**
- * Log a 1st-party event for internal analytics.
- * Events are batched and exported to /api/event_logging/batch
- *
- * @param eventName - Name of the event (e.g., 'tengu_api_query')
- * @param metadata - Additional metadata for the event (intentionally no strings, to avoid accidentally logging code/filepaths)
- */
 export function logEventTo1P(
-  eventName: string,
-  metadata: Record<string, number | boolean | undefined> = {},
-): void {
-  if (!is1PEventLoggingEnabled()) {
-    return
-  }
-
-  if (!firstPartyEventLogger || isSinkKilled('firstParty')) {
-    return
-  }
-
-  // Fire and forget - don't block on metadata enrichment
-  void logEventTo1PAsync(firstPartyEventLogger, eventName, metadata)
-}
+  _eventName: string,
+  _metadata: Record<string, number | boolean | undefined> = {},
+): void {}
 
-/**
- * GrowthBook experiment event data for logging
- */
 export type GrowthBookExperimentData = {
   experimentId: string
   variationId: number
@@ -239,211 +39,10 @@ export type GrowthBookExperimentData = {
   experimentMetadata?: Record<string, unknown>
 }
 
-// api.anthropic.com only serves the "production" GrowthBook environment
-// (see starling/starling/cli/cli.py DEFAULT_ENVIRONMENTS). Staging and
-// development environments are not exported to the prod API.
-function getEnvironmentForGrowthBook(): string {
-  return 'production'
-}
-
-/**
- * Log a GrowthBook experiment assignment event to 1P.
- * Events are batched and exported to /api/event_logging/batch
- *
- * @param data - GrowthBook experiment assignment data
- */
 export function logGrowthBookExperimentTo1P(
-  data: GrowthBookExperimentData,
-): void {
-  if (!is1PEventLoggingEnabled()) {
-    return
-  }
-
-  if (!firstPartyEventLogger || isSinkKilled('firstParty')) {
-    return
-  }
-
-  const userId = getOrCreateUserID()
-  const { accountUuid, organizationUuid } = getCoreUserData(true)
-
-  // Build attributes for GrowthbookExperimentEvent
-  const attributes = {
-    event_type: 'GrowthbookExperimentEvent',
-    event_id: randomUUID(),
-    experiment_id: data.experimentId,
-    variation_id: data.variationId,
-    ...(userId && { device_id: userId }),
-    ...(accountUuid && { account_uuid: accountUuid }),
-    ...(organizationUuid && { organization_uuid: organizationUuid }),
-    ...(data.userAttributes && {
-      session_id: data.userAttributes.sessionId,
-      user_attributes: jsonStringify(data.userAttributes),
-    }),
-    ...(data.experimentMetadata && {
-      experiment_metadata: jsonStringify(data.experimentMetadata),
-    }),
-    environment: getEnvironmentForGrowthBook(),
-  }
-
-  if (process.env.USER_TYPE === 'ant') {
-    logForDebugging(
-      `[ANT-ONLY] 1P GrowthBook experiment: ${data.experimentId} variation=${data.variationId}`,
-    )
-  }
-
-  firstPartyEventLogger.emit({
-    body: 'growthbook_experiment',
-    attributes,
-  })
-}
-
-const DEFAULT_LOGS_EXPORT_INTERVAL_MS = 10000
-const DEFAULT_MAX_EXPORT_BATCH_SIZE = 200
-const DEFAULT_MAX_QUEUE_SIZE = 8192
-
-/**
- * Initialize 1P event logging infrastructure.
- * This creates a separate LoggerProvider for internal event logging,
- * independent of customer OTLP telemetry.
- *
- * This uses its own minimal resource configuration with just the attributes
- * we need for internal analytics (service name, version, platform info).
- */
-export function initialize1PEventLogging(): void {
-  profileCheckpoint('1p_event_logging_start')
-  const enabled = is1PEventLoggingEnabled()
-
-  if (!enabled) {
-    if (process.env.USER_TYPE === 'ant') {
-      logForDebugging('1P event logging not enabled')
-    }
-    return
-  }
-
-  // Fetch batch processor configuration from GrowthBook dynamic config
-  // Uses cached value if available, refreshes in background
-  const batchConfig = getBatchConfig()
-  lastBatchConfig = batchConfig
-  profileCheckpoint('1p_event_after_growthbook_config')
-
-  const scheduledDelayMillis =
-    batchConfig.scheduledDelayMillis ||
-    parseInt(
-      process.env.OTEL_LOGS_EXPORT_INTERVAL ||
-        DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(),
-    )
+  _data: GrowthBookExperimentData,
+): void {}
 
-  const maxExportBatchSize =
-    batchConfig.maxExportBatchSize || DEFAULT_MAX_EXPORT_BATCH_SIZE
+export function initialize1PEventLogging(): void {}
 
-  const maxQueueSize = batchConfig.maxQueueSize || DEFAULT_MAX_QUEUE_SIZE
-
-  // Build our own resource for 1P event logging with minimal attributes
-  const platform = getPlatform()
-  const attributes: Record<string, string> = {
-    [ATTR_SERVICE_NAME]: 'claude-code',
-    [ATTR_SERVICE_VERSION]: MACRO.VERSION,
-  }
-
-  // Add WSL-specific attributes if running on WSL
-  if (platform === 'wsl') {
-    const wslVersion = getWslVersion()
-    if (wslVersion) {
-      attributes['wsl.version'] = wslVersion
-    }
-  }
-
-  const resource = resourceFromAttributes(attributes)
-
-  // Create a new LoggerProvider with the EventLoggingExporter
-  // NOTE: This is kept separate from customer telemetry logs to ensure
-  // internal events don't leak to customer endpoints and vice versa.
-  // We don't register this globally - it's only used for internal event logging.
-  const eventLoggingExporter = new FirstPartyEventLoggingExporter({
-    maxBatchSize: maxExportBatchSize,
-    skipAuth: batchConfig.skipAuth,
-    maxAttempts: batchConfig.maxAttempts,
-    path: batchConfig.path,
-    baseUrl: batchConfig.baseUrl,
-    isKilled: () => isSinkKilled('firstParty'),
-  })
-  firstPartyEventLoggerProvider = new LoggerProvider({
-    resource,
-    processors: [
-      new BatchLogRecordProcessor(eventLoggingExporter, {
-        scheduledDelayMillis,
-        maxExportBatchSize,
-        maxQueueSize,
-      }),
-    ],
-  })
-
-  // Initialize event logger from our internal provider (NOT from global API)
-  // IMPORTANT: We must get the logger from our local provider, not logs.getLogger()
-  // because logs.getLogger() returns a logger from the global provider, which is
-  // separate and used for customer telemetry.
-  firstPartyEventLogger = firstPartyEventLoggerProvider.getLogger(
-    'com.anthropic.claude_code.events',
-    MACRO.VERSION,
-  )
-}
-
-/**
- * Rebuild the 1P event logging pipeline if the batch config changed.
- * Register this with onGrowthBookRefresh so long-running sessions pick up
- * changes to batch size, delay, endpoint, etc.
- *
- * Event-loss safety:
- * 1. Null the logger first — concurrent logEventTo1P() calls hit the
- *    !firstPartyEventLogger guard and bail during the swap window. This drops
- *    a handful of events but prevents emitting to a draining provider.
- * 2. forceFlush() drains the old BatchLogRecordProcessor buffer to the
- *    exporter. Export failures go to disk at getCurrentBatchFilePath() which
- *    is keyed by module-level BATCH_UUID + sessionId — unchanged across
- *    reinit — so the NEW exporter's disk-backed retry picks them up.
- * 3. Swap to new provider/logger; old provider shutdown runs in background
- *    (buffer already drained, just cleanup).
- */
-export async function reinitialize1PEventLoggingIfConfigChanged(): Promise<void> {
-  if (!is1PEventLoggingEnabled() || !firstPartyEventLoggerProvider) {
-    return
-  }
-
-  const newConfig = getBatchConfig()
-
-  if (isEqual(newConfig, lastBatchConfig)) {
-    return
-  }
-
-  if (process.env.USER_TYPE === 'ant') {
-    logForDebugging(
-      `1P event logging: ${BATCH_CONFIG_NAME} changed, reinitializing`,
-    )
-  }
-
-  const oldProvider = firstPartyEventLoggerProvider
-  const oldLogger = firstPartyEventLogger
-  firstPartyEventLogger = null
-
-  try {
-    await oldProvider.forceFlush()
-  } catch {
-    // Export failures are already on disk; new exporter will retry them.
-  }
-
-  firstPartyEventLoggerProvider = null
-  try {
-    initialize1PEventLogging()
-  } catch (e) {
-    // Restore so the next GrowthBook refresh can retry. oldProvider was
-    // only forceFlush()'d, not shut down — it's still functional. Without
-    // this, both stay null and the !firstPartyEventLoggerProvider gate at
-    // the top makes recovery impossible.
-    firstPartyEventLoggerProvider = oldProvider
-    firstPartyEventLogger = oldLogger
-    logError(e)
-    return
-  }
-
-  void oldProvider.shutdown().catch(() => {})
-}
+export async function reinitialize1PEventLoggingIfConfigChanged(): Promise<void> {}

+ 12 - 145
src/services/analytics/index.ts

@@ -1,74 +1,22 @@
 /**
  * Analytics service - public API for event logging
  *
- * This module serves as the main entry point for analytics events in Claude CLI.
- *
- * DESIGN: This module has NO dependencies to avoid import cycles.
- * Events are queued until attachAnalyticsSink() is called during app initialization.
- * The sink handles routing to Datadog and 1P event logging.
+ * The open build intentionally ships without product telemetry. We keep this
+ * module as a compatibility boundary so existing call sites can remain
+ * unchanged while all analytics become inert.
  */
 
-/**
- * Marker type for verifying analytics metadata doesn't contain sensitive data
- *
- * This type forces explicit verification that string values being logged
- * don't contain code snippets, file paths, or other sensitive information.
- *
- * Usage: `myString as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS`
- */
 export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never
-
-/**
- * Marker type for values routed to PII-tagged proto columns via `_PROTO_*`
- * payload keys. The destination BQ column has privileged access controls,
- * so unredacted values are acceptable — unlike general-access backends.
- *
- * sink.ts strips `_PROTO_*` keys before Datadog fanout; only the 1P
- * exporter (firstPartyEventLoggingExporter) sees them and hoists them to the
- * top-level proto field. A single stripProtoFields call guards all non-1P
- * sinks — no per-sink filtering to forget.
- *
- * Usage: `rawName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED`
- */
 export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
 
-/**
- * Strip `_PROTO_*` keys from a payload destined for general-access storage.
- * Used by:
- *   - sink.ts: before Datadog fanout (never sees PII-tagged values)
- *   - firstPartyEventLoggingExporter: defensive strip of additional_metadata
- *     after hoisting known _PROTO_* keys to proto fields — prevents a future
- *     unrecognized _PROTO_foo from silently landing in the BQ JSON blob.
- *
- * Returns the input unchanged (same reference) when no _PROTO_ keys present.
- */
 export function stripProtoFields<V>(
   metadata: Record<string, V>,
 ): Record<string, V> {
-  let result: Record<string, V> | undefined
-  for (const key in metadata) {
-    if (key.startsWith('_PROTO_')) {
-      if (result === undefined) {
-        result = { ...metadata }
-      }
-      delete result[key]
-    }
-  }
-  return result ?? metadata
+  return metadata
 }
 
-// Internal type for logEvent metadata - different from the enriched EventMetadata in metadata.ts
 type LogEventMetadata = { [key: string]: boolean | number | undefined }
 
-type QueuedEvent = {
-  eventName: string
-  metadata: LogEventMetadata
-  async: boolean
-}
-
-/**
- * Sink interface for the analytics backend
- */
 export type AnalyticsSink = {
   logEvent: (eventName: string, metadata: LogEventMetadata) => void
   logEventAsync: (
@@ -77,97 +25,16 @@ export type AnalyticsSink = {
   ) => Promise<void>
 }
 
-// Event queue for events logged before sink is attached
-const eventQueue: QueuedEvent[] = []
-
-// Sink - initialized during app startup
-let sink: AnalyticsSink | null = null
+export function attachAnalyticsSink(_newSink: AnalyticsSink): void {}
 
-/**
- * Attach the analytics sink that will receive all events.
- * Queued events are drained asynchronously via queueMicrotask to avoid
- * adding latency to the startup path.
- *
- * Idempotent: if a sink is already attached, this is a no-op. This allows
- * calling from both the preAction hook (for subcommands) and setup() (for
- * the default command) without coordination.
- */
-export function attachAnalyticsSink(newSink: AnalyticsSink): void {
-  if (sink !== null) {
-    return
-  }
-  sink = newSink
-
-  // Drain the queue asynchronously to avoid blocking startup
-  if (eventQueue.length > 0) {
-    const queuedEvents = [...eventQueue]
-    eventQueue.length = 0
-
-    // Log queue size for ants to help debug analytics initialization timing
-    if (process.env.USER_TYPE === 'ant') {
-      sink.logEvent('analytics_sink_attached', {
-        queued_event_count: queuedEvents.length,
-      })
-    }
-
-    queueMicrotask(() => {
-      for (const event of queuedEvents) {
-        if (event.async) {
-          void sink!.logEventAsync(event.eventName, event.metadata)
-        } else {
-          sink!.logEvent(event.eventName, event.metadata)
-        }
-      }
-    })
-  }
-}
-
-/**
- * Log an event to analytics backends (synchronous)
- *
- * Events may be sampled based on the 'tengu_event_sampling_config' dynamic config.
- * When sampled, the sample_rate is added to the event metadata.
- *
- * If no sink is attached, events are queued and drained when the sink attaches.
- */
 export function logEvent(
-  eventName: string,
-  // intentionally no strings unless AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
-  // to avoid accidentally logging code/filepaths
-  metadata: LogEventMetadata,
-): void {
-  if (sink === null) {
-    eventQueue.push({ eventName, metadata, async: false })
-    return
-  }
-  sink.logEvent(eventName, metadata)
-}
+  _eventName: string,
+  _metadata: LogEventMetadata,
+): void {}
 
-/**
- * Log an event to analytics backends (asynchronous)
- *
- * Events may be sampled based on the 'tengu_event_sampling_config' dynamic config.
- * When sampled, the sample_rate is added to the event metadata.
- *
- * If no sink is attached, events are queued and drained when the sink attaches.
- */
 export async function logEventAsync(
-  eventName: string,
-  // intentionally no strings, to avoid accidentally logging code/filepaths
-  metadata: LogEventMetadata,
-): Promise<void> {
-  if (sink === null) {
-    eventQueue.push({ eventName, metadata, async: true })
-    return
-  }
-  await sink.logEventAsync(eventName, metadata)
-}
+  _eventName: string,
+  _metadata: LogEventMetadata,
+): Promise<void> {}
 
-/**
- * Reset analytics state for testing purposes only.
- * @internal
- */
-export function _resetForTesting(): void {
-  sink = null
-  eventQueue.length = 0
-}
+export function _resetForTesting(): void {}

+ 4 - 108
src/services/analytics/sink.ts

@@ -1,114 +1,10 @@
 /**
  * Analytics sink implementation
  *
- * This module contains the actual analytics routing logic and should be
- * initialized during app startup. It routes events to Datadog and 1P event
- * logging.
- *
- * Usage: Call initializeAnalyticsSink() during app startup to attach the sink.
- */
-
-import { trackDatadogEvent } from './datadog.js'
-import { logEventTo1P, shouldSampleEvent } from './firstPartyEventLogger.js'
-import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from './growthbook.js'
-import { attachAnalyticsSink, stripProtoFields } from './index.js'
-import { isSinkKilled } from './sinkKillswitch.js'
-
-// Local type matching the logEvent metadata signature
-type LogEventMetadata = { [key: string]: boolean | number | undefined }
-
-const DATADOG_GATE_NAME = 'tengu_log_datadog_events'
-
-// Module-level gate state - starts undefined, initialized during startup
-let isDatadogGateEnabled: boolean | undefined = undefined
-
-/**
- * Check if Datadog tracking is enabled.
- * Falls back to cached value from previous session if not yet initialized.
- */
-function shouldTrackDatadog(): boolean {
-  if (isSinkKilled('datadog')) {
-    return false
-  }
-  if (isDatadogGateEnabled !== undefined) {
-    return isDatadogGateEnabled
-  }
-
-  // Fallback to cached value from previous session
-  try {
-    return checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME)
-  } catch {
-    return false
-  }
-}
-
-/**
- * Log an event (synchronous implementation)
- */
-function logEventImpl(eventName: string, metadata: LogEventMetadata): void {
-  // Check if this event should be sampled
-  const sampleResult = shouldSampleEvent(eventName)
-
-  // If sample result is 0, the event was not selected for logging
-  if (sampleResult === 0) {
-    return
-  }
-
-  // If sample result is a positive number, add it to metadata
-  const metadataWithSampleRate =
-    sampleResult !== null
-      ? { ...metadata, sample_rate: sampleResult }
-      : metadata
-
-  if (shouldTrackDatadog()) {
-    // Datadog is a general-access backend — strip _PROTO_* keys
-    // (unredacted PII-tagged values meant only for the 1P privileged column).
-    void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate))
-  }
-
-  // 1P receives the full payload including _PROTO_* — the exporter
-  // destructures and routes those keys to proto fields itself.
-  logEventTo1P(eventName, metadataWithSampleRate)
-}
-
-/**
- * Log an event (asynchronous implementation)
- *
- * With Segment removed the two remaining sinks are fire-and-forget, so this
- * just wraps the sync impl — kept to preserve the sink interface contract.
+ * Telemetry sinks are disabled in this build. The exported functions remain so
+ * startup code does not need to special-case the OSS variant.
  */
-function logEventAsyncImpl(
-  eventName: string,
-  metadata: LogEventMetadata,
-): Promise<void> {
-  logEventImpl(eventName, metadata)
-  return Promise.resolve()
-}
 
-/**
- * Initialize analytics gates during startup.
- *
- * Updates gate values from server. Early events use cached values from previous
- * session to avoid data loss during initialization.
- *
- * Called from main.tsx during setupBackend().
- */
-export function initializeAnalyticsGates(): void {
-  isDatadogGateEnabled =
-    checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME)
-}
+export function initializeAnalyticsGates(): void {}
 
-/**
- * Initialize the analytics sink.
- *
- * Call this during app startup to attach the analytics backend.
- * Any events logged before this is called will be queued and drained.
- *
- * Idempotent: safe to call multiple times (subsequent calls are no-ops).
- */
-export function initializeAnalyticsSink(): void {
-  attachAnalyticsSink({
-    logEvent: logEventImpl,
-    logEventAsync: logEventAsyncImpl,
-  })
-}
+export function initializeAnalyticsSink(): void {}

+ 39 - 466
src/utils/telemetry/betaSessionTracing.ts

@@ -1,491 +1,64 @@
 /**
- * Beta Session Tracing for Claude Code
- *
- * This module contains beta tracing features enabled when
- * ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT are set.
- *
- * For external users, tracing is enabled in SDK/headless mode, or in
- * interactive mode when the org is allowlisted via the
- * tengu_trace_lantern GrowthBook gate.
- * For ant users, tracing is enabled in all modes.
- *
- * Visibility Rules:
- * | Content          | External | Ant  |
- * |------------------|----------|------|
- * | System prompts   | ✅                  | ✅   |
- * | Model output     | ✅                  | ✅   |
- * | Thinking output  | ❌                  | ✅   |
- * | Tools            | ✅                  | ✅   |
- * | new_context      | ✅                  | ✅   |
- *
- * Features:
- * - Per-agent message tracking with hash-based deduplication
- * - System prompt logging (once per unique hash)
- * - Hook execution spans
- * - Detailed new_context attributes for LLM requests
- */
-
-import type { Span } from '@opentelemetry/api'
-import { createHash } from 'crypto'
-import { getIsNonInteractiveSession } from '../../bootstrap/state.js'
-import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
-import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js'
-import type { AssistantMessage, UserMessage } from '../../types/message.js'
-import { isEnvTruthy } from '../envUtils.js'
-import { jsonParse, jsonStringify } from '../slowOperations.js'
-import { logOTelEvent } from './events.js'
-
-// Message type for API calls (UserMessage or AssistantMessage)
-type APIMessage = UserMessage | AssistantMessage
-
-/**
- * Track hashes we've already logged this session (system prompts, tools, etc).
- *
- * WHY: System prompts and tool schemas are large and rarely change within a session.
- * Sending full content on every request would be wasteful. Instead, we hash and
- * only log the full content once per unique hash.
- */
-const seenHashes = new Set<string>()
-
-/**
- * Track the last reported message hash per querySource (agent) for incremental context.
- *
- * WHY: When debugging traces, we want to see what NEW information was added each turn,
- * not the entire conversation history (which can be huge). By tracking the last message
- * we reported per agent, we can compute and send only the delta (new messages since
- * the last request). This is tracked per-agent (querySource) because different agents
- * (main thread, subagents, warmup requests) have independent conversation contexts.
- */
-const lastReportedMessageHash = new Map<string, string>()
+ * Beta tracing is disabled in the OSS build.
+ */
+
+export interface Span {
+  setAttribute(name: string, value: string | number | boolean): void
+  setAttributes(attributes: Record<string, string | number | boolean>): void
+  addEvent(
+    name: string,
+    attributes?: Record<string, string | number | boolean>,
+  ): void
+  end(): void
+  recordException(error: unknown): void
+}
 
-/**
- * Clear tracking state after compaction.
- * Old hashes are irrelevant once messages have been replaced.
- */
-export function clearBetaTracingState(): void {
-  seenHashes.clear()
-  lastReportedMessageHash.clear()
+export interface LLMRequestNewContext {
+  systemPrompt?: string
+  querySource?: string
+  tools?: string
 }
 
-const MAX_CONTENT_SIZE = 60 * 1024 // 60KB (Honeycomb limit is 64KB, staying safe)
+export function clearBetaTracingState(): void {}
 
-/**
- * Check if beta detailed tracing is enabled.
- * - Requires ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT
- * - For external users, enabled in SDK/headless mode OR when org is
- *   allowlisted via the tengu_trace_lantern GrowthBook gate
- */
 export function isBetaTracingEnabled(): boolean {
-  const baseEnabled =
-    isEnvTruthy(process.env.ENABLE_BETA_TRACING_DETAILED) &&
-    Boolean(process.env.BETA_TRACING_ENDPOINT)
-
-  if (!baseEnabled) {
-    return false
-  }
-
-  // For external users, enable in SDK/headless mode OR when org is allowlisted.
-  // Gate reads from disk cache, so first run after allowlisting returns false;
-  // works from second run onward (same behavior as enhanced_telemetry_beta).
-  if (process.env.USER_TYPE !== 'ant') {
-    return (
-      getIsNonInteractiveSession() ||
-      getFeatureValue_CACHED_MAY_BE_STALE('tengu_trace_lantern', false)
-    )
-  }
-
-  return true
+  return false
 }
 
-/**
- * Truncate content to fit within Honeycomb limits.
- */
 export function truncateContent(
   content: string,
-  maxSize: number = MAX_CONTENT_SIZE,
+  _maxSize?: number,
 ): { content: string; truncated: boolean } {
-  if (content.length <= maxSize) {
-    return { content, truncated: false }
-  }
-
-  return {
-    content:
-      content.slice(0, maxSize) +
-      '\n\n[TRUNCATED - Content exceeds 60KB limit]',
-    truncated: true,
-  }
+  return { content, truncated: false }
 }
 
-/**
- * Generate a short hash (first 12 hex chars of SHA-256).
- */
-function shortHash(content: string): string {
-  return createHash('sha256').update(content).digest('hex').slice(0, 12)
-}
-
-/**
- * Generate a hash for a system prompt.
- */
-function hashSystemPrompt(systemPrompt: string): string {
-  return `sp_${shortHash(systemPrompt)}`
-}
-
-/**
- * Generate a hash for a message based on its content.
- */
-function hashMessage(message: APIMessage): string {
-  const content = jsonStringify(message.message.content)
-  return `msg_${shortHash(content)}`
-}
-
-// Regex to detect content wrapped in <system-reminder> tags
-const SYSTEM_REMINDER_REGEX =
-  /^<system-reminder>\n?([\s\S]*?)\n?<\/system-reminder>$/
-
-/**
- * Check if text is entirely a system reminder (wrapped in <system-reminder> tags).
- * Returns the inner content if it is, null otherwise.
- */
-function extractSystemReminderContent(text: string): string | null {
-  const match = text.trim().match(SYSTEM_REMINDER_REGEX)
-  return match && match[1] ? match[1].trim() : null
-}
-
-/**
- * Result of formatting messages - separates regular content from system reminders.
- */
-interface FormattedMessages {
-  contextParts: string[]
-  systemReminders: string[]
-}
-
-/**
- * Format user messages for new_context display, separating system reminders.
- * Only handles user messages (assistant messages are filtered out before this is called).
- */
-function formatMessagesForContext(messages: UserMessage[]): FormattedMessages {
-  const contextParts: string[] = []
-  const systemReminders: string[] = []
-
-  for (const message of messages) {
-    const content = message.message.content
-    if (typeof content === 'string') {
-      const reminderContent = extractSystemReminderContent(content)
-      if (reminderContent) {
-        systemReminders.push(reminderContent)
-      } else {
-        contextParts.push(`[USER]\n${content}`)
-      }
-    } else if (Array.isArray(content)) {
-      for (const block of content) {
-        if (block.type === 'text') {
-          const reminderContent = extractSystemReminderContent(block.text)
-          if (reminderContent) {
-            systemReminders.push(reminderContent)
-          } else {
-            contextParts.push(`[USER]\n${block.text}`)
-          }
-        } else if (block.type === 'tool_result') {
-          const resultContent =
-            typeof block.content === 'string'
-              ? block.content
-              : jsonStringify(block.content)
-          // Tool results can also contain system reminders (e.g., malware warning)
-          const reminderContent = extractSystemReminderContent(resultContent)
-          if (reminderContent) {
-            systemReminders.push(reminderContent)
-          } else {
-            contextParts.push(
-              `[TOOL RESULT: ${block.tool_use_id}]\n${resultContent}`,
-            )
-          }
-        }
-      }
-    }
-  }
-
-  return { contextParts, systemReminders }
-}
-
-export interface LLMRequestNewContext {
-  /** System prompt (typically only on first request or if changed) */
-  systemPrompt?: string
-  /** Query source identifying the agent/purpose (e.g., 'repl_main_thread', 'agent:builtin') */
-  querySource?: string
-  /** Tool schemas sent with the request */
-  tools?: string
-}
-
-/**
- * Add beta attributes to an interaction span.
- * Adds new_context with the user prompt.
- */
 export function addBetaInteractionAttributes(
-  span: Span,
-  userPrompt: string,
-): void {
-  if (!isBetaTracingEnabled()) {
-    return
-  }
-
-  const { content: truncatedPrompt, truncated } = truncateContent(
-    `[USER PROMPT]\n${userPrompt}`,
-  )
-  span.setAttributes({
-    new_context: truncatedPrompt,
-    ...(truncated && {
-      new_context_truncated: true,
-      new_context_original_length: userPrompt.length,
-    }),
-  })
-}
+  _span: Span,
+  _userPrompt: string,
+): void {}
 
-/**
- * Add beta attributes to an LLM request span.
- * Handles system prompt logging and new_context computation.
- */
 export function addBetaLLMRequestAttributes(
-  span: Span,
-  newContext?: LLMRequestNewContext,
-  messagesForAPI?: APIMessage[],
-): void {
-  if (!isBetaTracingEnabled()) {
-    return
-  }
-
-  // Add system prompt info to the span
-  if (newContext?.systemPrompt) {
-    const promptHash = hashSystemPrompt(newContext.systemPrompt)
-    const preview = newContext.systemPrompt.slice(0, 500)
-
-    // Always add hash, preview, and length to the span
-    span.setAttribute('system_prompt_hash', promptHash)
-    span.setAttribute('system_prompt_preview', preview)
-    span.setAttribute('system_prompt_length', newContext.systemPrompt.length)
-
-    // Log the full system prompt only once per unique hash this session
-    if (!seenHashes.has(promptHash)) {
-      seenHashes.add(promptHash)
-
-      // Truncate for the log if needed
-      const { content: truncatedPrompt, truncated } = truncateContent(
-        newContext.systemPrompt,
-      )
-
-      void logOTelEvent('system_prompt', {
-        system_prompt_hash: promptHash,
-        system_prompt: truncatedPrompt,
-        system_prompt_length: String(newContext.systemPrompt.length),
-        ...(truncated && { system_prompt_truncated: 'true' }),
-      })
-    }
-  }
-
-  // Add tools info to the span
-  if (newContext?.tools) {
-    try {
-      const toolsArray = jsonParse(newContext.tools) as Record<
-        string,
-        unknown
-      >[]
-
-      // Build array of {name, hash} for each tool
-      const toolsWithHashes = toolsArray.map(tool => {
-        const toolJson = jsonStringify(tool)
-        const toolHash = shortHash(toolJson)
-        return {
-          name: typeof tool.name === 'string' ? tool.name : 'unknown',
-          hash: toolHash,
-          json: toolJson,
-        }
-      })
-
-      // Set span attribute with array of name/hash pairs
-      span.setAttribute(
-        'tools',
-        jsonStringify(
-          toolsWithHashes.map(({ name, hash }) => ({ name, hash })),
-        ),
-      )
-      span.setAttribute('tools_count', toolsWithHashes.length)
-
-      // Log each tool's full description once per unique hash
-      for (const { name, hash, json } of toolsWithHashes) {
-        if (!seenHashes.has(`tool_${hash}`)) {
-          seenHashes.add(`tool_${hash}`)
-
-          const { content: truncatedTool, truncated } = truncateContent(json)
+  _span: Span,
+  _newContext?: LLMRequestNewContext,
+  _messagesForAPI?: unknown[],
+): void {}
 
-          void logOTelEvent('tool', {
-            tool_name: sanitizeToolNameForAnalytics(name),
-            tool_hash: hash,
-            tool: truncatedTool,
-            ...(truncated && { tool_truncated: 'true' }),
-          })
-        }
-      }
-    } catch {
-      // If parsing fails, log the raw tools string
-      span.setAttribute('tools_parse_error', true)
-    }
-  }
-
-  // Add new_context using hash-based tracking (visible to all users)
-  if (messagesForAPI && messagesForAPI.length > 0 && newContext?.querySource) {
-    const querySource = newContext.querySource
-    const lastHash = lastReportedMessageHash.get(querySource)
-
-    // Find where the last reported message is in the array
-    let startIndex = 0
-    if (lastHash) {
-      for (let i = 0; i < messagesForAPI.length; i++) {
-        const msg = messagesForAPI[i]
-        if (msg && hashMessage(msg) === lastHash) {
-          startIndex = i + 1 // Start after the last reported message
-          break
-        }
-      }
-      // If lastHash not found, startIndex stays 0 (send everything)
-    }
-
-    // Get new messages (filter out assistant messages - we only want user input/tool results)
-    const newMessages = messagesForAPI
-      .slice(startIndex)
-      .filter((m): m is UserMessage => m.type === 'user')
-
-    if (newMessages.length > 0) {
-      // Format new messages, separating system reminders from regular content
-      const { contextParts, systemReminders } =
-        formatMessagesForContext(newMessages)
-
-      // Set new_context (regular user content and tool results)
-      if (contextParts.length > 0) {
-        const fullContext = contextParts.join('\n\n---\n\n')
-        const { content: truncatedContext, truncated } =
-          truncateContent(fullContext)
-
-        span.setAttributes({
-          new_context: truncatedContext,
-          new_context_message_count: newMessages.length,
-          ...(truncated && {
-            new_context_truncated: true,
-            new_context_original_length: fullContext.length,
-          }),
-        })
-      }
-
-      // Set system_reminders as a separate attribute
-      if (systemReminders.length > 0) {
-        const fullReminders = systemReminders.join('\n\n---\n\n')
-        const { content: truncatedReminders, truncated: remindersTruncated } =
-          truncateContent(fullReminders)
-
-        span.setAttributes({
-          system_reminders: truncatedReminders,
-          system_reminders_count: systemReminders.length,
-          ...(remindersTruncated && {
-            system_reminders_truncated: true,
-            system_reminders_original_length: fullReminders.length,
-          }),
-        })
-      }
-
-      // Update last reported hash to the last message in the array
-      const lastMessage = messagesForAPI[messagesForAPI.length - 1]
-      if (lastMessage) {
-        lastReportedMessageHash.set(querySource, hashMessage(lastMessage))
-      }
-    }
-  }
-}
-
-/**
- * Add beta attributes to endLLMRequestSpan.
- * Handles model_output and thinking_output truncation.
- */
 export function addBetaLLMResponseAttributes(
-  endAttributes: Record<string, string | number | boolean>,
-  metadata?: {
+  _endAttributes: Record<string, string | number | boolean>,
+  _metadata?: {
     modelOutput?: string
     thinkingOutput?: string
   },
-): void {
-  if (!isBetaTracingEnabled() || !metadata) {
-    return
-  }
-
-  // Add model_output (text content) - visible to all users
-  if (metadata.modelOutput !== undefined) {
-    const { content: modelOutput, truncated: outputTruncated } =
-      truncateContent(metadata.modelOutput)
-    endAttributes['response.model_output'] = modelOutput
-    if (outputTruncated) {
-      endAttributes['response.model_output_truncated'] = true
-      endAttributes['response.model_output_original_length'] =
-        metadata.modelOutput.length
-    }
-  }
-
-  // Add thinking_output - ant-only
-  if (
-    process.env.USER_TYPE === 'ant' &&
-    metadata.thinkingOutput !== undefined
-  ) {
-    const { content: thinkingOutput, truncated: thinkingTruncated } =
-      truncateContent(metadata.thinkingOutput)
-    endAttributes['response.thinking_output'] = thinkingOutput
-    if (thinkingTruncated) {
-      endAttributes['response.thinking_output_truncated'] = true
-      endAttributes['response.thinking_output_original_length'] =
-        metadata.thinkingOutput.length
-    }
-  }
-}
+): void {}
 
-/**
- * Add beta attributes to startToolSpan.
- * Adds tool_input with the serialized tool input.
- */
 export function addBetaToolInputAttributes(
-  span: Span,
-  toolName: string,
-  toolInput: string,
-): void {
-  if (!isBetaTracingEnabled()) {
-    return
-  }
+  _span: Span,
+  _toolName: string,
+  _toolInput: string,
+) {}
 
-  const { content: truncatedInput, truncated } = truncateContent(
-    `[TOOL INPUT: ${toolName}]\n${toolInput}`,
-  )
-  span.setAttributes({
-    tool_input: truncatedInput,
-    ...(truncated && {
-      tool_input_truncated: true,
-      tool_input_original_length: toolInput.length,
-    }),
-  })
-}
-
-/**
- * Add beta attributes to endToolSpan.
- * Adds new_context with the tool result.
- */
 export function addBetaToolResultAttributes(
-  endAttributes: Record<string, string | number | boolean>,
-  toolName: string | number | boolean,
-  toolResult: string,
-): void {
-  if (!isBetaTracingEnabled()) {
-    return
-  }
-
-  const { content: truncatedResult, truncated } = truncateContent(
-    `[TOOL RESULT: ${toolName}]\n${toolResult}`,
-  )
-  endAttributes['new_context'] = truncatedResult
-  if (truncated) {
-    endAttributes['new_context_truncated'] = true
-    endAttributes['new_context_original_length'] = toolResult.length
-  }
-}
+  _endAttributes: Record<string, string | number | boolean>,
+  _toolName: string | number | boolean,
+  _toolResult: string,
+): void {}

+ 5 - 68
src/utils/telemetry/events.ts

@@ -1,17 +1,5 @@
-import type { Attributes } from '@opentelemetry/api'
-import { getEventLogger, getPromptId } from 'src/bootstrap/state.js'
-import { logForDebugging } from '../debug.js'
-import { isEnvTruthy } from '../envUtils.js'
-import { getTelemetryAttributes } from '../telemetryAttributes.js'
-
-// Monotonically increasing counter for ordering events within a session
-let eventSequence = 0
-
-// Track whether we've already warned about a null event logger to avoid spamming
-let hasWarnedNoEventLogger = false
-
-function isUserPromptLoggingEnabled() {
-  return isEnvTruthy(process.env.OTEL_LOG_USER_PROMPTS)
+function isUserPromptLoggingEnabled(): boolean {
+  return false
 }
 
 export function redactIfDisabled(content: string): string {
@@ -19,57 +7,6 @@ export function redactIfDisabled(content: string): string {
 }
 
 export async function logOTelEvent(
-  eventName: string,
-  metadata: { [key: string]: string | undefined } = {},
-): Promise<void> {
-  const eventLogger = getEventLogger()
-  if (!eventLogger) {
-    if (!hasWarnedNoEventLogger) {
-      hasWarnedNoEventLogger = true
-      logForDebugging(
-        `[3P telemetry] Event dropped (no event logger initialized): ${eventName}`,
-        { level: 'warn' },
-      )
-    }
-    return
-  }
-
-  // Skip logging in test environment
-  if (process.env.NODE_ENV === 'test') {
-    return
-  }
-
-  const attributes: Attributes = {
-    ...getTelemetryAttributes(),
-    'event.name': eventName,
-    'event.timestamp': new Date().toISOString(),
-    'event.sequence': eventSequence++,
-  }
-
-  // Add prompt ID to events (but not metrics, where it would cause unbounded cardinality)
-  const promptId = getPromptId()
-  if (promptId) {
-    attributes['prompt.id'] = promptId
-  }
-
-  // Workspace directory from the desktop app (host path). Events only —
-  // filesystem paths are too high-cardinality for metric dimensions, and
-  // the BQ metrics pipeline must never see them.
-  const workspaceDir = process.env.CLAUDE_CODE_WORKSPACE_HOST_PATHS
-  if (workspaceDir) {
-    attributes['workspace.host_paths'] = workspaceDir.split('|')
-  }
-
-  // Add metadata as attributes - all values are already strings
-  for (const [key, value] of Object.entries(metadata)) {
-    if (value !== undefined) {
-      attributes[key] = value
-    }
-  }
-
-  // Emit log record as an event
-  eventLogger.emit({
-    body: `claude_code.${eventName}`,
-    attributes,
-  })
-}
+  _eventName: string,
+  _metadata: { [key: string]: string | undefined } = {},
+): Promise<void> {}

+ 11 - 812
src/utils/telemetry/instrumentation.ts

@@ -1,825 +1,24 @@
-import { DiagLogLevel, diag, trace } from '@opentelemetry/api'
-import { logs } from '@opentelemetry/api-logs'
-// OTLP/Prometheus exporters are dynamically imported inside the protocol
-// switch statements below. A process uses at most one protocol variant per
-// signal, but static imports would load all 6 (~1.2MB) on every startup.
-import {
-  envDetector,
-  hostDetector,
-  osDetector,
-  resourceFromAttributes,
-} from '@opentelemetry/resources'
-import {
-  BatchLogRecordProcessor,
-  ConsoleLogRecordExporter,
-  LoggerProvider,
-} from '@opentelemetry/sdk-logs'
-import {
-  ConsoleMetricExporter,
-  MeterProvider,
-  PeriodicExportingMetricReader,
-} from '@opentelemetry/sdk-metrics'
-import {
-  BasicTracerProvider,
-  BatchSpanProcessor,
-  ConsoleSpanExporter,
-} from '@opentelemetry/sdk-trace-base'
-import {
-  ATTR_SERVICE_NAME,
-  ATTR_SERVICE_VERSION,
-  SEMRESATTRS_HOST_ARCH,
-} from '@opentelemetry/semantic-conventions'
-import { HttpsProxyAgent } from 'https-proxy-agent'
-import {
-  getLoggerProvider,
-  getMeterProvider,
-  getTracerProvider,
-  setEventLogger,
-  setLoggerProvider,
-  setMeterProvider,
-  setTracerProvider,
-} from 'src/bootstrap/state.js'
-import {
-  getOtelHeadersFromHelper,
-  getSubscriptionType,
-  is1PApiCustomer,
-  isClaudeAISubscriber,
-} from 'src/utils/auth.js'
-import { getPlatform, getWslVersion } from 'src/utils/platform.js'
-
-import { getCACertificates } from '../caCerts.js'
-import { registerCleanup } from '../cleanupRegistry.js'
-import { getHasFormattedOutput, logForDebugging } from '../debug.js'
-import { isEnvTruthy } from '../envUtils.js'
-import { errorMessage } from '../errors.js'
-import { getMTLSConfig } from '../mtls.js'
-import { getProxyUrl, shouldBypassProxy } from '../proxy.js'
-import { getSettings_DEPRECATED } from '../settings/settings.js'
-import { jsonStringify } from '../slowOperations.js'
-import { profileCheckpoint } from '../startupProfiler.js'
-import { isBetaTracingEnabled } from './betaSessionTracing.js'
-import { BigQueryMetricsExporter } from './bigqueryExporter.js'
-import { ClaudeCodeDiagLogger } from './logger.js'
-import { initializePerfettoTracing } from './perfettoTracing.js'
-import {
-  endInteractionSpan,
-  isEnhancedTelemetryEnabled,
-} from './sessionTracing.js'
-
-const DEFAULT_METRICS_EXPORT_INTERVAL_MS = 60000
-const DEFAULT_LOGS_EXPORT_INTERVAL_MS = 5000
-const DEFAULT_TRACES_EXPORT_INTERVAL_MS = 5000
-
-class TelemetryTimeoutError extends Error {}
-
-function telemetryTimeout(ms: number, message: string): Promise<never> {
-  return new Promise((_, reject) => {
-    setTimeout(
-      (rej: (e: Error) => void, msg: string) =>
-        rej(new TelemetryTimeoutError(msg)),
-      ms,
-      reject,
-      message,
-    ).unref()
-  })
-}
-
-export function bootstrapTelemetry() {
-  if (process.env.USER_TYPE === 'ant') {
-    // Read from ANT_ prefixed variables that are defined at build time
-    if (process.env.ANT_OTEL_METRICS_EXPORTER) {
-      process.env.OTEL_METRICS_EXPORTER = process.env.ANT_OTEL_METRICS_EXPORTER
-    }
-    if (process.env.ANT_OTEL_LOGS_EXPORTER) {
-      process.env.OTEL_LOGS_EXPORTER = process.env.ANT_OTEL_LOGS_EXPORTER
-    }
-    if (process.env.ANT_OTEL_TRACES_EXPORTER) {
-      process.env.OTEL_TRACES_EXPORTER = process.env.ANT_OTEL_TRACES_EXPORTER
-    }
-    if (process.env.ANT_OTEL_EXPORTER_OTLP_PROTOCOL) {
-      process.env.OTEL_EXPORTER_OTLP_PROTOCOL =
-        process.env.ANT_OTEL_EXPORTER_OTLP_PROTOCOL
-    }
-    if (process.env.ANT_OTEL_EXPORTER_OTLP_ENDPOINT) {
-      process.env.OTEL_EXPORTER_OTLP_ENDPOINT =
-        process.env.ANT_OTEL_EXPORTER_OTLP_ENDPOINT
-    }
-    if (process.env.ANT_OTEL_EXPORTER_OTLP_HEADERS) {
-      process.env.OTEL_EXPORTER_OTLP_HEADERS =
-        process.env.ANT_OTEL_EXPORTER_OTLP_HEADERS
-    }
-  }
+/**
+ * Telemetry instrumentation is disabled in the OSS build.
+ */
 
-  // Set default tempoality to 'delta' because it's the more sane default
-  if (!process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE) {
-    process.env.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = 'delta'
-  }
-}
+export function bootstrapTelemetry(): void {}
 
-// Per OTEL spec, "none" means "no automatically configured exporter for this signal".
-// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection
 export function parseExporterTypes(value: string | undefined): string[] {
   return (value || '')
     .trim()
     .split(',')
     .filter(Boolean)
-    .map(t => t.trim())
-    .filter(t => t !== 'none')
-}
-
-async function getOtlpReaders() {
-  const exporterTypes = parseExporterTypes(process.env.OTEL_METRICS_EXPORTER)
-  const exportInterval = parseInt(
-    process.env.OTEL_METRIC_EXPORT_INTERVAL ||
-      DEFAULT_METRICS_EXPORT_INTERVAL_MS.toString(),
-  )
-
-  const exporters = []
-  for (const exporterType of exporterTypes) {
-    if (exporterType === 'console') {
-      // Custom console exporter that shows resource attributes
-      const consoleExporter = new ConsoleMetricExporter()
-      const originalExport = consoleExporter.export.bind(consoleExporter)
-
-      consoleExporter.export = (metrics, callback) => {
-        // Log resource attributes once at the start
-        if (metrics.resource && metrics.resource.attributes) {
-          // The console exporter is for debugging, so console output is intentional here
-
-          logForDebugging('\n=== Resource Attributes ===')
-          logForDebugging(jsonStringify(metrics.resource.attributes))
-          logForDebugging('===========================\n')
-        }
-
-        return originalExport(metrics, callback)
-      }
-
-      exporters.push(consoleExporter)
-    } else if (exporterType === 'otlp') {
-      const protocol =
-        process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL?.trim() ||
-        process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim()
-
-      const httpConfig = getOTLPExporterConfig()
-
-      switch (protocol) {
-        case 'grpc': {
-          // Lazy-import to keep @grpc/grpc-js (~700KB) out of the telemetry chunk
-          // when the protocol is http/protobuf (ant default) or http/json.
-          const { OTLPMetricExporter } = await import(
-            '@opentelemetry/exporter-metrics-otlp-grpc'
-          )
-          exporters.push(new OTLPMetricExporter())
-          break
-        }
-        case 'http/json': {
-          const { OTLPMetricExporter } = await import(
-            '@opentelemetry/exporter-metrics-otlp-http'
-          )
-          exporters.push(new OTLPMetricExporter(httpConfig))
-          break
-        }
-        case 'http/protobuf': {
-          const { OTLPMetricExporter } = await import(
-            '@opentelemetry/exporter-metrics-otlp-proto'
-          )
-          exporters.push(new OTLPMetricExporter(httpConfig))
-          break
-        }
-        default:
-          throw new Error(
-            `Unknown protocol set in OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`,
-          )
-      }
-    } else if (exporterType === 'prometheus') {
-      const { PrometheusExporter } = await import(
-        '@opentelemetry/exporter-prometheus'
-      )
-      exporters.push(new PrometheusExporter())
-    } else {
-      throw new Error(
-        `Unknown exporter type set in OTEL_EXPORTER_OTLP_METRICS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${exporterType}`,
-      )
-    }
-  }
-
-  return exporters.map(exporter => {
-    if ('export' in exporter) {
-      return new PeriodicExportingMetricReader({
-        exporter,
-        exportIntervalMillis: exportInterval,
-      })
-    }
-    return exporter
-  })
-}
-
-async function getOtlpLogExporters() {
-  const exporterTypes = parseExporterTypes(process.env.OTEL_LOGS_EXPORTER)
-
-  const protocol =
-    process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL?.trim() ||
-    process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim()
-  const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT
-
-  logForDebugging(
-    `[3P telemetry] getOtlpLogExporters: types=${jsonStringify(exporterTypes)}, protocol=${protocol}, endpoint=${endpoint}`,
-  )
-
-  const exporters = []
-  for (const exporterType of exporterTypes) {
-    if (exporterType === 'console') {
-      exporters.push(new ConsoleLogRecordExporter())
-    } else if (exporterType === 'otlp') {
-      const httpConfig = getOTLPExporterConfig()
-
-      switch (protocol) {
-        case 'grpc': {
-          const { OTLPLogExporter } = await import(
-            '@opentelemetry/exporter-logs-otlp-grpc'
-          )
-          exporters.push(new OTLPLogExporter())
-          break
-        }
-        case 'http/json': {
-          const { OTLPLogExporter } = await import(
-            '@opentelemetry/exporter-logs-otlp-http'
-          )
-          exporters.push(new OTLPLogExporter(httpConfig))
-          break
-        }
-        case 'http/protobuf': {
-          const { OTLPLogExporter } = await import(
-            '@opentelemetry/exporter-logs-otlp-proto'
-          )
-          exporters.push(new OTLPLogExporter(httpConfig))
-          break
-        }
-        default:
-          throw new Error(
-            `Unknown protocol set in OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`,
-          )
-      }
-    } else {
-      throw new Error(
-        `Unknown exporter type set in OTEL_LOGS_EXPORTER env var: ${exporterType}`,
-      )
-    }
-  }
-
-  return exporters
-}
-
-async function getOtlpTraceExporters() {
-  const exporterTypes = parseExporterTypes(process.env.OTEL_TRACES_EXPORTER)
-
-  const exporters = []
-  for (const exporterType of exporterTypes) {
-    if (exporterType === 'console') {
-      exporters.push(new ConsoleSpanExporter())
-    } else if (exporterType === 'otlp') {
-      const protocol =
-        process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL?.trim() ||
-        process.env.OTEL_EXPORTER_OTLP_PROTOCOL?.trim()
-
-      const httpConfig = getOTLPExporterConfig()
-
-      switch (protocol) {
-        case 'grpc': {
-          const { OTLPTraceExporter } = await import(
-            '@opentelemetry/exporter-trace-otlp-grpc'
-          )
-          exporters.push(new OTLPTraceExporter())
-          break
-        }
-        case 'http/json': {
-          const { OTLPTraceExporter } = await import(
-            '@opentelemetry/exporter-trace-otlp-http'
-          )
-          exporters.push(new OTLPTraceExporter(httpConfig))
-          break
-        }
-        case 'http/protobuf': {
-          const { OTLPTraceExporter } = await import(
-            '@opentelemetry/exporter-trace-otlp-proto'
-          )
-          exporters.push(new OTLPTraceExporter(httpConfig))
-          break
-        }
-        default:
-          throw new Error(
-            `Unknown protocol set in OTEL_EXPORTER_OTLP_TRACES_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL env var: ${protocol}`,
-          )
-      }
-    } else {
-      throw new Error(
-        `Unknown exporter type set in OTEL_TRACES_EXPORTER env var: ${exporterType}`,
-      )
-    }
-  }
-
-  return exporters
-}
-
-export function isTelemetryEnabled() {
-  return isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_TELEMETRY)
-}
-
-function getBigQueryExportingReader() {
-  const bigqueryExporter = new BigQueryMetricsExporter()
-  return new PeriodicExportingMetricReader({
-    exporter: bigqueryExporter,
-    exportIntervalMillis: 5 * 60 * 1000, // 5mins for BigQuery metrics exporter to reduce load
-  })
-}
-
-function isBigQueryMetricsEnabled() {
-  // BigQuery metrics are enabled for:
-  // 1. API customers (excluding Claude.ai subscribers and Bedrock/Vertex)
-  // 2. Claude for Enterprise (C4E) users
-  // 3. Claude for Teams users
-  const subscriptionType = getSubscriptionType()
-  const isC4EOrTeamUser =
-    isClaudeAISubscriber() &&
-    (subscriptionType === 'enterprise' || subscriptionType === 'team')
-
-  return is1PApiCustomer() || isC4EOrTeamUser
-}
-
-/**
- * Initialize beta tracing - a separate code path for detailed debugging.
- * Uses BETA_TRACING_ENDPOINT instead of OTEL_EXPORTER_OTLP_ENDPOINT.
- */
-async function initializeBetaTracing(
-  resource: ReturnType<typeof resourceFromAttributes>,
-): Promise<void> {
-  const endpoint = process.env.BETA_TRACING_ENDPOINT
-  if (!endpoint) {
-    return
-  }
-
-  const [{ OTLPTraceExporter }, { OTLPLogExporter }] = await Promise.all([
-    import('@opentelemetry/exporter-trace-otlp-http'),
-    import('@opentelemetry/exporter-logs-otlp-http'),
-  ])
-
-  const httpConfig = {
-    url: `${endpoint}/v1/traces`,
-  }
-
-  const logHttpConfig = {
-    url: `${endpoint}/v1/logs`,
-  }
-
-  // Initialize trace exporter
-  const traceExporter = new OTLPTraceExporter(httpConfig)
-  const spanProcessor = new BatchSpanProcessor(traceExporter, {
-    scheduledDelayMillis: DEFAULT_TRACES_EXPORT_INTERVAL_MS,
-  })
-
-  const tracerProvider = new BasicTracerProvider({
-    resource,
-    spanProcessors: [spanProcessor],
-  })
-
-  trace.setGlobalTracerProvider(tracerProvider)
-  setTracerProvider(tracerProvider)
-
-  // Initialize log exporter
-  const logExporter = new OTLPLogExporter(logHttpConfig)
-  const loggerProvider = new LoggerProvider({
-    resource,
-    processors: [
-      new BatchLogRecordProcessor(logExporter, {
-        scheduledDelayMillis: DEFAULT_LOGS_EXPORT_INTERVAL_MS,
-      }),
-    ],
-  })
-
-  logs.setGlobalLoggerProvider(loggerProvider)
-  setLoggerProvider(loggerProvider)
-
-  // Initialize event logger
-  const eventLogger = logs.getLogger(
-    'com.anthropic.claude_code.events',
-    MACRO.VERSION,
-  )
-  setEventLogger(eventLogger)
-
-  // Setup flush handlers - flush both logs AND traces
-  process.on('beforeExit', async () => {
-    await loggerProvider?.forceFlush()
-    await tracerProvider?.forceFlush()
-  })
-
-  process.on('exit', () => {
-    void loggerProvider?.forceFlush()
-    void tracerProvider?.forceFlush()
-  })
-}
-
-export async function initializeTelemetry() {
-  profileCheckpoint('telemetry_init_start')
-  bootstrapTelemetry()
-
-  // Console exporters call console.dir on a timer (5s logs/traces, 60s
-  // metrics), writing pretty-printed objects to stdout. In stream-json
-  // mode stdout is the SDK message channel; the first line (`{`) breaks
-  // the SDK's line reader. Stripped here (not main.tsx) because init.ts
-  // re-runs applyConfigEnvironmentVariables() inside initializeTelemetry-
-  // AfterTrust for remote-managed-settings users, and bootstrapTelemetry
-  // above copies ANT_OTEL_* for ant users — both would undo an earlier strip.
-  if (getHasFormattedOutput()) {
-    for (const key of [
-      'OTEL_METRICS_EXPORTER',
-      'OTEL_LOGS_EXPORTER',
-      'OTEL_TRACES_EXPORTER',
-    ] as const) {
-      const v = process.env[key]
-      if (v?.includes('console')) {
-        process.env[key] = v
-          .split(',')
-          .map(s => s.trim())
-          .filter(s => s !== 'console')
-          .join(',')
-      }
-    }
-  }
-
-  diag.setLogger(new ClaudeCodeDiagLogger(), DiagLogLevel.ERROR)
-
-  // Initialize Perfetto tracing (independent of OTEL)
-  // Enable via CLAUDE_CODE_PERFETTO_TRACE=1 or CLAUDE_CODE_PERFETTO_TRACE=<path>
-  initializePerfettoTracing()
-
-  const readers = []
-
-  // Add customer exporters (if enabled)
-  const telemetryEnabled = isTelemetryEnabled()
-  logForDebugging(
-    `[3P telemetry] isTelemetryEnabled=${telemetryEnabled} (CLAUDE_CODE_ENABLE_TELEMETRY=${process.env.CLAUDE_CODE_ENABLE_TELEMETRY})`,
-  )
-  if (telemetryEnabled) {
-    readers.push(...(await getOtlpReaders()))
-  }
-
-  // Add BigQuery exporter (for API customers, C4E users, and internal users)
-  if (isBigQueryMetricsEnabled()) {
-    readers.push(getBigQueryExportingReader())
-  }
-
-  // Create base resource with service attributes
-  const platform = getPlatform()
-  const baseAttributes: Record<string, string> = {
-    [ATTR_SERVICE_NAME]: 'claude-code',
-    [ATTR_SERVICE_VERSION]: MACRO.VERSION,
-  }
-
-  // Add WSL-specific attributes if running on WSL
-  if (platform === 'wsl') {
-    const wslVersion = getWslVersion()
-    if (wslVersion) {
-      baseAttributes['wsl.version'] = wslVersion
-    }
-  }
-
-  const baseResource = resourceFromAttributes(baseAttributes)
-
-  // Use OpenTelemetry detectors
-  const osResource = resourceFromAttributes(
-    osDetector.detect().attributes || {},
-  )
-
-  // Extract only host.arch from hostDetector
-  const hostDetected = hostDetector.detect()
-  const hostArchAttributes = hostDetected.attributes?.[SEMRESATTRS_HOST_ARCH]
-    ? {
-        [SEMRESATTRS_HOST_ARCH]: hostDetected.attributes[SEMRESATTRS_HOST_ARCH],
-      }
-    : {}
-  const hostArchResource = resourceFromAttributes(hostArchAttributes)
-
-  const envResource = resourceFromAttributes(
-    envDetector.detect().attributes || {},
-  )
-
-  // Merge resources - later resources take precedence
-  const resource = baseResource
-    .merge(osResource)
-    .merge(hostArchResource)
-    .merge(envResource)
-
-  // Check if beta tracing is enabled - this is a separate code path
-  // Available to all users who set ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT
-  if (isBetaTracingEnabled()) {
-    void initializeBetaTracing(resource).catch(e =>
-      logForDebugging(`Beta tracing init failed: ${e}`, { level: 'error' }),
-    )
-    // Still set up meter provider for metrics (but skip regular logs/traces setup)
-    const meterProvider = new MeterProvider({
-      resource,
-      views: [],
-      readers,
-    })
-    setMeterProvider(meterProvider)
-
-    // Register shutdown for beta tracing
-    const shutdownTelemetry = async () => {
-      const timeoutMs = parseInt(
-        process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000',
-      )
-      try {
-        endInteractionSpan()
-
-        // Force flush + shutdown together inside the timeout. Previously forceFlush
-        // was awaited unbounded BEFORE the race, blocking exit on slow OTLP endpoints.
-        // Each provider's flush→shutdown is chained independently so a slow logger
-        // flush doesn't delay meterProvider/tracerProvider shutdown (no waterfall).
-        const loggerProvider = getLoggerProvider()
-        const tracerProvider = getTracerProvider()
-
-        const chains: Promise<void>[] = [meterProvider.shutdown()]
-        if (loggerProvider) {
-          chains.push(
-            loggerProvider.forceFlush().then(() => loggerProvider.shutdown()),
-          )
-        }
-        if (tracerProvider) {
-          chains.push(
-            tracerProvider.forceFlush().then(() => tracerProvider.shutdown()),
-          )
-        }
-
-        await Promise.race([
-          Promise.all(chains),
-          telemetryTimeout(timeoutMs, 'OpenTelemetry shutdown timeout'),
-        ])
-      } catch {
-        // Ignore shutdown errors
-      }
-    }
-    registerCleanup(shutdownTelemetry)
-
-    return meterProvider.getMeter('com.anthropic.claude_code', MACRO.VERSION)
-  }
-
-  const meterProvider = new MeterProvider({
-    resource,
-    views: [],
-    readers,
-  })
-
-  // Store reference in state for flushing
-  setMeterProvider(meterProvider)
-
-  // Initialize logs if telemetry is enabled
-  if (telemetryEnabled) {
-    const logExporters = await getOtlpLogExporters()
-    logForDebugging(
-      `[3P telemetry] Created ${logExporters.length} log exporter(s)`,
-    )
-
-    if (logExporters.length > 0) {
-      const loggerProvider = new LoggerProvider({
-        resource,
-        // Add batch processors for each exporter
-        processors: logExporters.map(
-          exporter =>
-            new BatchLogRecordProcessor(exporter, {
-              scheduledDelayMillis: parseInt(
-                process.env.OTEL_LOGS_EXPORT_INTERVAL ||
-                  DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(),
-              ),
-            }),
-        ),
-      })
-
-      // Register the logger provider globally
-      logs.setGlobalLoggerProvider(loggerProvider)
-      setLoggerProvider(loggerProvider)
-
-      // Initialize event logger
-      const eventLogger = logs.getLogger(
-        'com.anthropic.claude_code.events',
-        MACRO.VERSION,
-      )
-      setEventLogger(eventLogger)
-      logForDebugging('[3P telemetry] Event logger set successfully')
-
-      // 'beforeExit' is emitted when Node.js empties its event loop and has no additional work to schedule.
-      // Unlike 'exit', it allows us to perform async operations, so it works well for letting
-      // network requests complete before the process exits naturally.
-      process.on('beforeExit', async () => {
-        await loggerProvider?.forceFlush()
-        // Also flush traces - they use BatchSpanProcessor which needs explicit flush
-        const tracerProvider = getTracerProvider()
-        await tracerProvider?.forceFlush()
-      })
-
-      process.on('exit', () => {
-        // Final attempt to flush logs and traces
-        void loggerProvider?.forceFlush()
-        void getTracerProvider()?.forceFlush()
-      })
-    }
-  }
-
-  // Initialize tracing if enhanced telemetry is enabled (BETA)
-  if (telemetryEnabled && isEnhancedTelemetryEnabled()) {
-    const traceExporters = await getOtlpTraceExporters()
-    if (traceExporters.length > 0) {
-      // Create span processors for each exporter
-      const spanProcessors = traceExporters.map(
-        exporter =>
-          new BatchSpanProcessor(exporter, {
-            scheduledDelayMillis: parseInt(
-              process.env.OTEL_TRACES_EXPORT_INTERVAL ||
-                DEFAULT_TRACES_EXPORT_INTERVAL_MS.toString(),
-            ),
-          }),
-      )
-
-      const tracerProvider = new BasicTracerProvider({
-        resource,
-        spanProcessors,
-      })
-
-      // Register the tracer provider globally
-      trace.setGlobalTracerProvider(tracerProvider)
-      setTracerProvider(tracerProvider)
-    }
-  }
-
-  // Shutdown metrics and logs on exit (flushes and closes exporters)
-  const shutdownTelemetry = async () => {
-    const timeoutMs = parseInt(
-      process.env.CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS || '2000',
-    )
-
-    try {
-      // End any active interaction span before shutdown
-      endInteractionSpan()
-
-      const shutdownPromises = [meterProvider.shutdown()]
-      const loggerProvider = getLoggerProvider()
-      if (loggerProvider) {
-        shutdownPromises.push(loggerProvider.shutdown())
-      }
-      const tracerProvider = getTracerProvider()
-      if (tracerProvider) {
-        shutdownPromises.push(tracerProvider.shutdown())
-      }
-
-      await Promise.race([
-        Promise.all(shutdownPromises),
-        telemetryTimeout(timeoutMs, 'OpenTelemetry shutdown timeout'),
-      ])
-    } catch (error) {
-      if (error instanceof Error && error.message.includes('timeout')) {
-        logForDebugging(
-          `
-OpenTelemetry telemetry flush timed out after ${timeoutMs}ms
-
-To resolve this issue, you can:
-1. Increase the timeout by setting CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS env var (e.g., 5000 for 5 seconds)
-2. Check if your OpenTelemetry backend is experiencing scalability issues
-3. Disable OpenTelemetry by unsetting CLAUDE_CODE_ENABLE_TELEMETRY env var
-
-Current timeout: ${timeoutMs}ms
-`,
-          { level: 'error' },
-        )
-      }
-      throw error
-    }
-  }
-
-  // Always register shutdown (internal metrics are always enabled)
-  registerCleanup(shutdownTelemetry)
-
-  return meterProvider.getMeter('com.anthropic.claude_code', MACRO.VERSION)
+    .map(token => token.trim())
+    .filter(token => token !== 'none')
 }
 
-/**
- * Flush all pending telemetry data immediately.
- * This should be called before logout or org switching to prevent data leakage.
- */
-export async function flushTelemetry(): Promise<void> {
-  const meterProvider = getMeterProvider()
-  if (!meterProvider) {
-    return
-  }
-
-  const timeoutMs = parseInt(
-    process.env.CLAUDE_CODE_OTEL_FLUSH_TIMEOUT_MS || '5000',
-  )
-
-  try {
-    const flushPromises = [meterProvider.forceFlush()]
-    const loggerProvider = getLoggerProvider()
-    if (loggerProvider) {
-      flushPromises.push(loggerProvider.forceFlush())
-    }
-    const tracerProvider = getTracerProvider()
-    if (tracerProvider) {
-      flushPromises.push(tracerProvider.forceFlush())
-    }
-
-    await Promise.race([
-      Promise.all(flushPromises),
-      telemetryTimeout(timeoutMs, 'OpenTelemetry flush timeout'),
-    ])
-
-    logForDebugging('Telemetry flushed successfully')
-  } catch (error) {
-    if (error instanceof TelemetryTimeoutError) {
-      logForDebugging(
-        `Telemetry flush timed out after ${timeoutMs}ms. Some metrics may not be exported.`,
-        { level: 'warn' },
-      )
-    } else {
-      logForDebugging(`Telemetry flush failed: ${errorMessage(error)}`, {
-        level: 'error',
-      })
-    }
-    // Don't throw - allow logout to continue even if flush fails
-  }
+export function isTelemetryEnabled(): boolean {
+  return false
 }
 
-function parseOtelHeadersEnvVar(): Record<string, string> {
-  const headers: Record<string, string> = {}
-  const envHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS
-  if (envHeaders) {
-    for (const pair of envHeaders.split(',')) {
-      const [key, ...valueParts] = pair.split('=')
-      if (key && valueParts.length > 0) {
-        headers[key.trim()] = valueParts.join('=').trim()
-      }
-    }
-  }
-  return headers
+export async function initializeTelemetry(): Promise<null> {
+  return null
 }
 
-/**
- * Get configuration for OTLP exporters including:
- * - HTTP agent options (proxy, mTLS)
- * - Dynamic headers via otelHeadersHelper or static headers from env var
- */
-function getOTLPExporterConfig() {
-  const proxyUrl = getProxyUrl()
-  const mtlsConfig = getMTLSConfig()
-  const settings = getSettings_DEPRECATED()
-
-  // Build base config
-  const config: Record<string, unknown> = {}
-
-  // Parse static headers from env var once (doesn't change at runtime)
-  const staticHeaders = parseOtelHeadersEnvVar()
-
-  // If otelHeadersHelper is configured, use async headers function for dynamic refresh
-  // Otherwise just return static headers if any exist
-  if (settings?.otelHeadersHelper) {
-    config.headers = async (): Promise<Record<string, string>> => {
-      const dynamicHeaders = getOtelHeadersFromHelper()
-      return { ...staticHeaders, ...dynamicHeaders }
-    }
-  } else if (Object.keys(staticHeaders).length > 0) {
-    config.headers = async (): Promise<Record<string, string>> => staticHeaders
-  }
-
-  // Check if we should bypass proxy for OTEL endpoint
-  const otelEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT
-  if (!proxyUrl || (otelEndpoint && shouldBypassProxy(otelEndpoint))) {
-    // No proxy configured or OTEL endpoint should bypass proxy
-    const caCerts = getCACertificates()
-    if (mtlsConfig || caCerts) {
-      config.httpAgentOptions = {
-        ...mtlsConfig,
-        ...(caCerts && { ca: caCerts }),
-      }
-    }
-    return config
-  }
-
-  // Return an HttpAgentFactory function that creates our proxy agent
-  const caCerts = getCACertificates()
-  const agentFactory = (_protocol: string) => {
-    // Create and return the proxy agent with mTLS and CA cert config
-    const proxyAgent =
-      mtlsConfig || caCerts
-        ? new HttpsProxyAgent(proxyUrl, {
-            ...(mtlsConfig && {
-              cert: mtlsConfig.cert,
-              key: mtlsConfig.key,
-              passphrase: mtlsConfig.passphrase,
-            }),
-            ...(caCerts && { ca: caCerts }),
-          })
-        : new HttpsProxyAgent(proxyUrl)
-
-    return proxyAgent
-  }
-
-  config.httpAgentOptions = agentFactory
-  return config
-}
+export async function flushTelemetry(): Promise<void> {}

+ 64 - 844
src/utils/telemetry/sessionTracing.ts

@@ -1,358 +1,62 @@
-/**
- * Session Tracing for Claude Code using OpenTelemetry (BETA)
- *
- * This module provides a high-level API for creating and managing spans
- * to trace Claude Code workflows. Each user interaction creates a root
- * interaction span, which contains operation spans (LLM requests, tool calls, etc.).
- *
- * Requirements:
- * - Enhanced telemetry is enabled via feature('ENHANCED_TELEMETRY_BETA')
- * - Configure OTEL_TRACES_EXPORTER (console, otlp, etc.)
- */
-
-import { feature } from 'bun:bundle'
-import { context as otelContext, type Span, trace } from '@opentelemetry/api'
-import { AsyncLocalStorage } from 'async_hooks'
-import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
-import type { AssistantMessage, UserMessage } from '../../types/message.js'
-import { isEnvDefinedFalsy, isEnvTruthy } from '../envUtils.js'
-import { getTelemetryAttributes } from '../telemetryAttributes.js'
 import {
-  addBetaInteractionAttributes,
-  addBetaLLMRequestAttributes,
-  addBetaLLMResponseAttributes,
-  addBetaToolInputAttributes,
-  addBetaToolResultAttributes,
   isBetaTracingEnabled,
   type LLMRequestNewContext,
-  truncateContent,
 } from './betaSessionTracing.js'
-import {
-  endInteractionPerfettoSpan,
-  endLLMRequestPerfettoSpan,
-  endToolPerfettoSpan,
-  endUserInputPerfettoSpan,
-  isPerfettoTracingEnabled,
-  startInteractionPerfettoSpan,
-  startLLMRequestPerfettoSpan,
-  startToolPerfettoSpan,
-  startUserInputPerfettoSpan,
-} from './perfettoTracing.js'
 
-// Re-export for callers
-export type { Span }
 export { isBetaTracingEnabled, type LLMRequestNewContext }
 
-// Message type for API calls (UserMessage or AssistantMessage)
-type APIMessage = UserMessage | AssistantMessage
-
-type SpanType =
-  | 'interaction'
-  | 'llm_request'
-  | 'tool'
-  | 'tool.blocked_on_user'
-  | 'tool.execution'
-  | 'hook'
-
-interface SpanContext {
-  span: Span
-  startTime: number
-  attributes: Record<string, string | number | boolean>
-  ended?: boolean
-  perfettoSpanId?: string
+export interface Span {
+  setAttribute(name: string, value: string | number | boolean): void
+  setAttributes(attributes: Record<string, string | number | boolean>): void
+  addEvent(
+    name: string,
+    attributes?: Record<string, string | number | boolean>,
+  ): void
+  end(): void
+  recordException(error: unknown): void
 }
 
-// ALS stores SpanContext directly so it holds a strong reference while a span
-// is active. With that, activeSpans can use WeakRef — when ALS is cleared
-// (enterWith(undefined)) and no other code holds the SpanContext, GC can collect
-// it and the WeakRef goes stale.
-const interactionContext = new AsyncLocalStorage<SpanContext | undefined>()
-const toolContext = new AsyncLocalStorage<SpanContext | undefined>()
-const activeSpans = new Map<string, WeakRef<SpanContext>>()
-// Spans not stored in ALS (LLM request, blocked-on-user, tool execution, hook)
-// need a strong reference to prevent GC from collecting the SpanContext before
-// the corresponding end* function retrieves it.
-const strongSpans = new Map<string, SpanContext>()
-let interactionSequence = 0
-let _cleanupIntervalStarted = false
-
-const SPAN_TTL_MS = 30 * 60 * 1000 // 30 minutes
-
-function getSpanId(span: Span): string {
-  return span.spanContext().spanId || ''
-}
+type SpanMetadata = Record<string, string | number | boolean>
 
-/**
- * Lazily start a background interval that evicts orphaned spans from activeSpans.
- *
- * Normal teardown calls endInteractionSpan / endToolSpan, which delete spans
- * immediately. This interval is a safety net for spans that were never ended
- * (e.g. aborted streams, uncaught exceptions mid-query) — without it they
- * accumulate in activeSpans indefinitely, holding references to Span objects
- * and the OpenTelemetry context chain.
- *
- * Initialized on the first startInteractionSpan call (not at module load) to
- * avoid triggering the no-top-level-side-effects lint rule and to keep the
- * interval from running in processes that never start a span.
- * unref() prevents the timer from keeping the process alive after all other
- * work is done.
- */
-function ensureCleanupInterval(): void {
-  if (_cleanupIntervalStarted) return
-  _cleanupIntervalStarted = true
-  const interval = setInterval(() => {
-    const cutoff = Date.now() - SPAN_TTL_MS
-    for (const [spanId, weakRef] of activeSpans) {
-      const ctx = weakRef.deref()
-      if (ctx === undefined) {
-        activeSpans.delete(spanId)
-        strongSpans.delete(spanId)
-      } else if (ctx.startTime < cutoff) {
-        if (!ctx.ended) ctx.span.end() // flush any recorded attributes to the exporter
-        activeSpans.delete(spanId)
-        strongSpans.delete(spanId)
-      }
-    }
-  }, 60_000)
-  if (typeof interval.unref === 'function') {
-    interval.unref() // Node.js / Bun: don't block process exit
+function createNoopSpan(): Span {
+  return {
+    setAttribute() {},
+    setAttributes() {},
+    addEvent() {},
+    end() {},
+    recordException() {},
   }
 }
 
-/**
- * Check if enhanced telemetry is enabled.
- * Priority: env var override > ant build > GrowthBook gate
- */
+let currentInteractionSpan: Span | null = null
+let currentToolSpan: Span | null = null
+
 export function isEnhancedTelemetryEnabled(): boolean {
-  if (feature('ENHANCED_TELEMETRY_BETA')) {
-    const env =
-      process.env.CLAUDE_CODE_ENHANCED_TELEMETRY_BETA ??
-      process.env.ENABLE_ENHANCED_TELEMETRY_BETA
-    if (isEnvTruthy(env)) {
-      return true
-    }
-    if (isEnvDefinedFalsy(env)) {
-      return false
-    }
-    return (
-      process.env.USER_TYPE === 'ant' ||
-      getFeatureValue_CACHED_MAY_BE_STALE('enhanced_telemetry_beta', false)
-    )
-  }
   return false
 }
 
-/**
- * Check if any tracing is enabled (either standard enhanced telemetry OR beta tracing)
- */
-function isAnyTracingEnabled(): boolean {
-  return isEnhancedTelemetryEnabled() || isBetaTracingEnabled()
-}
-
-function getTracer() {
-  return trace.getTracer('com.anthropic.claude_code.tracing', '1.0.0')
-}
-
-function createSpanAttributes(
-  spanType: SpanType,
-  customAttributes: Record<string, string | number | boolean> = {},
-): Record<string, string | number | boolean> {
-  const baseAttributes = getTelemetryAttributes()
-
-  const attributes: Record<string, string | number | boolean> = {
-    ...baseAttributes,
-    'span.type': spanType,
-    ...customAttributes,
-  }
-
-  return attributes
-}
-
-/**
- * Start an interaction span. This wraps a user request -> Claude response cycle.
- * This is now a root span that includes all session-level attributes.
- * Sets the interaction context for all subsequent operations.
- */
-export function startInteractionSpan(userPrompt: string): Span {
-  ensureCleanupInterval()
-
-  // Start Perfetto span regardless of OTel tracing state
-  const perfettoSpanId = isPerfettoTracingEnabled()
-    ? startInteractionPerfettoSpan(userPrompt)
-    : undefined
-
-  if (!isAnyTracingEnabled()) {
-    // Still track Perfetto span even if OTel is disabled
-    if (perfettoSpanId) {
-      const dummySpan = trace.getActiveSpan() || getTracer().startSpan('dummy')
-      const spanId = getSpanId(dummySpan)
-      const spanContextObj: SpanContext = {
-        span: dummySpan,
-        startTime: Date.now(),
-        attributes: {},
-        perfettoSpanId,
-      }
-      activeSpans.set(spanId, new WeakRef(spanContextObj))
-      interactionContext.enterWith(spanContextObj)
-      return dummySpan
-    }
-    return trace.getActiveSpan() || getTracer().startSpan('dummy')
-  }
-
-  const tracer = getTracer()
-  const isUserPromptLoggingEnabled = isEnvTruthy(
-    process.env.OTEL_LOG_USER_PROMPTS,
-  )
-  const promptToLog = isUserPromptLoggingEnabled ? userPrompt : '<REDACTED>'
-
-  interactionSequence++
-
-  const attributes = createSpanAttributes('interaction', {
-    user_prompt: promptToLog,
-    user_prompt_length: userPrompt.length,
-    'interaction.sequence': interactionSequence,
-  })
-
-  const span = tracer.startSpan('claude_code.interaction', {
-    attributes,
-  })
-
-  // Add experimental attributes (new_context)
-  addBetaInteractionAttributes(span, userPrompt)
-
-  const spanId = getSpanId(span)
-  const spanContextObj: SpanContext = {
-    span,
-    startTime: Date.now(),
-    attributes,
-    perfettoSpanId,
-  }
-  activeSpans.set(spanId, new WeakRef(spanContextObj))
-
-  interactionContext.enterWith(spanContextObj)
-
+export function startInteractionSpan(_userPrompt: string): Span {
+  const span = createNoopSpan()
+  currentInteractionSpan = span
   return span
 }
 
 export function endInteractionSpan(): void {
-  const spanContext = interactionContext.getStore()
-  if (!spanContext) {
-    return
-  }
-
-  if (spanContext.ended) {
-    return
-  }
-
-  // End Perfetto span
-  if (spanContext.perfettoSpanId) {
-    endInteractionPerfettoSpan(spanContext.perfettoSpanId)
-  }
-
-  if (!isAnyTracingEnabled()) {
-    spanContext.ended = true
-    activeSpans.delete(getSpanId(spanContext.span))
-    // Clear the store so async continuations created after this point (timers,
-    // promise callbacks, I/O) do not inherit a reference to the ended span.
-    // enterWith(undefined) is intentional: exit(() => {}) is a no-op because it
-    // only suppresses the store inside the callback and returns immediately.
-    interactionContext.enterWith(undefined)
-    return
-  }
-
-  const duration = Date.now() - spanContext.startTime
-  spanContext.span.setAttributes({
-    'interaction.duration_ms': duration,
-  })
-
-  spanContext.span.end()
-  spanContext.ended = true
-  activeSpans.delete(getSpanId(spanContext.span))
-  interactionContext.enterWith(undefined)
+  currentInteractionSpan = null
 }
 
 export function startLLMRequestSpan(
-  model: string,
-  newContext?: LLMRequestNewContext,
-  messagesForAPI?: APIMessage[],
-  fastMode?: boolean,
+  _model: string,
+  _newContext?: LLMRequestNewContext,
+  _messagesForAPI?: unknown[],
+  _fastMode?: boolean,
 ): Span {
-  // Start Perfetto span regardless of OTel tracing state
-  const perfettoSpanId = isPerfettoTracingEnabled()
-    ? startLLMRequestPerfettoSpan({
-        model,
-        querySource: newContext?.querySource,
-        messageId: undefined, // Will be set in endLLMRequestSpan
-      })
-    : undefined
-
-  if (!isAnyTracingEnabled()) {
-    // Still track Perfetto span even if OTel is disabled
-    if (perfettoSpanId) {
-      const dummySpan = trace.getActiveSpan() || getTracer().startSpan('dummy')
-      const spanId = getSpanId(dummySpan)
-      const spanContextObj: SpanContext = {
-        span: dummySpan,
-        startTime: Date.now(),
-        attributes: { model },
-        perfettoSpanId,
-      }
-      activeSpans.set(spanId, new WeakRef(spanContextObj))
-      strongSpans.set(spanId, spanContextObj)
-      return dummySpan
-    }
-    return trace.getActiveSpan() || getTracer().startSpan('dummy')
-  }
-
-  const tracer = getTracer()
-  const parentSpanCtx = interactionContext.getStore()
-
-  const attributes = createSpanAttributes('llm_request', {
-    model: model,
-    'llm_request.context': parentSpanCtx ? 'interaction' : 'standalone',
-    speed: fastMode ? 'fast' : 'normal',
-  })
-
-  const ctx = parentSpanCtx
-    ? trace.setSpan(otelContext.active(), parentSpanCtx.span)
-    : otelContext.active()
-  const span = tracer.startSpan('claude_code.llm_request', { attributes }, ctx)
-
-  // Add query_source (agent name) if provided
-  if (newContext?.querySource) {
-    span.setAttribute('query_source', newContext.querySource)
-  }
-
-  // Add experimental attributes (system prompt, new_context)
-  addBetaLLMRequestAttributes(span, newContext, messagesForAPI)
-
-  const spanId = getSpanId(span)
-  const spanContextObj: SpanContext = {
-    span,
-    startTime: Date.now(),
-    attributes,
-    perfettoSpanId,
-  }
-  activeSpans.set(spanId, new WeakRef(spanContextObj))
-  strongSpans.set(spanId, spanContextObj)
-
-  return span
+  return createNoopSpan()
 }
 
-/**
- * End an LLM request span and attach response metadata.
- *
- * @param span - Optional. The exact span returned by startLLMRequestSpan().
- *   IMPORTANT: When multiple LLM requests run in parallel (e.g., warmup requests,
- *   topic classifier, file path extractor, main thread), you MUST pass the specific span
- *   to ensure responses are attached to the correct request. Without it, responses may be
- *   incorrectly attached to whichever span happens to be "last" in the activeSpans map.
- *
- *   If not provided, falls back to finding the most recent llm_request span (legacy behavior).
- */
 export function endLLMRequestSpan(
-  span?: Span,
-  metadata?: {
+  _span?: Span,
+  _metadata?: {
     inputTokens?: number
     outputTokens?: number
     cacheReadTokens?: number
@@ -362,566 +66,82 @@ export function endLLMRequestSpan(
     error?: string
     attempt?: number
     modelResponse?: string
-    /** Text output from the model (non-thinking content) */
     modelOutput?: string
-    /** Thinking/reasoning output from the model */
     thinkingOutput?: string
-    /** Whether the output included tool calls (look at tool spans for details) */
     hasToolCall?: boolean
-    /** Time to first token in milliseconds */
     ttftMs?: number
-    /** Time spent in pre-request setup before the successful attempt */
     requestSetupMs?: number
-    /** Timestamps (Date.now()) of each attempt start — used to emit retry sub-spans */
     attemptStartTimes?: number[]
   },
-): void {
-  let llmSpanContext: SpanContext | undefined
-
-  if (span) {
-    // Use the provided span directly - this is the correct approach for parallel requests
-    const spanId = getSpanId(span)
-    llmSpanContext = activeSpans.get(spanId)?.deref()
-  } else {
-    // Legacy fallback: find the most recent llm_request span
-    // WARNING: This can cause mismatched responses when multiple requests are in flight
-    llmSpanContext = Array.from(activeSpans.values())
-      .findLast(r => {
-        const ctx = r.deref()
-        return (
-          ctx?.attributes['span.type'] === 'llm_request' ||
-          ctx?.attributes['model']
-        )
-      })
-      ?.deref()
-  }
-
-  if (!llmSpanContext) {
-    // Span was already ended or never tracked
-    return
-  }
-
-  const duration = Date.now() - llmSpanContext.startTime
-
-  // End Perfetto span with full metadata
-  if (llmSpanContext.perfettoSpanId) {
-    endLLMRequestPerfettoSpan(llmSpanContext.perfettoSpanId, {
-      ttftMs: metadata?.ttftMs,
-      ttltMs: duration, // Time to last token is the total duration
-      promptTokens: metadata?.inputTokens,
-      outputTokens: metadata?.outputTokens,
-      cacheReadTokens: metadata?.cacheReadTokens,
-      cacheCreationTokens: metadata?.cacheCreationTokens,
-      success: metadata?.success,
-      error: metadata?.error,
-      requestSetupMs: metadata?.requestSetupMs,
-      attemptStartTimes: metadata?.attemptStartTimes,
-    })
-  }
-
-  if (!isAnyTracingEnabled()) {
-    const spanId = getSpanId(llmSpanContext.span)
-    activeSpans.delete(spanId)
-    strongSpans.delete(spanId)
-    return
-  }
-
-  const endAttributes: Record<string, string | number | boolean> = {
-    duration_ms: duration,
-  }
-
-  if (metadata) {
-    if (metadata.inputTokens !== undefined)
-      endAttributes['input_tokens'] = metadata.inputTokens
-    if (metadata.outputTokens !== undefined)
-      endAttributes['output_tokens'] = metadata.outputTokens
-    if (metadata.cacheReadTokens !== undefined)
-      endAttributes['cache_read_tokens'] = metadata.cacheReadTokens
-    if (metadata.cacheCreationTokens !== undefined)
-      endAttributes['cache_creation_tokens'] = metadata.cacheCreationTokens
-    if (metadata.success !== undefined)
-      endAttributes['success'] = metadata.success
-    if (metadata.statusCode !== undefined)
-      endAttributes['status_code'] = metadata.statusCode
-    if (metadata.error !== undefined) endAttributes['error'] = metadata.error
-    if (metadata.attempt !== undefined)
-      endAttributes['attempt'] = metadata.attempt
-    if (metadata.hasToolCall !== undefined)
-      endAttributes['response.has_tool_call'] = metadata.hasToolCall
-    if (metadata.ttftMs !== undefined)
-      endAttributes['ttft_ms'] = metadata.ttftMs
-
-    // Add experimental response attributes (model_output, thinking_output)
-    addBetaLLMResponseAttributes(endAttributes, metadata)
-  }
-
-  llmSpanContext.span.setAttributes(endAttributes)
-  llmSpanContext.span.end()
-
-  const spanId = getSpanId(llmSpanContext.span)
-  activeSpans.delete(spanId)
-  strongSpans.delete(spanId)
-}
+): void {}
 
 export function startToolSpan(
-  toolName: string,
-  toolAttributes?: Record<string, string | number | boolean>,
-  toolInput?: string,
+  _toolName: string,
+  _toolAttributes?: SpanMetadata,
+  _toolInput?: string,
 ): Span {
-  // Start Perfetto span regardless of OTel tracing state
-  const perfettoSpanId = isPerfettoTracingEnabled()
-    ? startToolPerfettoSpan(toolName, toolAttributes)
-    : undefined
-
-  if (!isAnyTracingEnabled()) {
-    // Still track Perfetto span even if OTel is disabled
-    if (perfettoSpanId) {
-      const dummySpan = trace.getActiveSpan() || getTracer().startSpan('dummy')
-      const spanId = getSpanId(dummySpan)
-      const spanContextObj: SpanContext = {
-        span: dummySpan,
-        startTime: Date.now(),
-        attributes: { 'span.type': 'tool', tool_name: toolName },
-        perfettoSpanId,
-      }
-      activeSpans.set(spanId, new WeakRef(spanContextObj))
-      toolContext.enterWith(spanContextObj)
-      return dummySpan
-    }
-    return trace.getActiveSpan() || getTracer().startSpan('dummy')
-  }
-
-  const tracer = getTracer()
-  const parentSpanCtx = interactionContext.getStore()
-
-  const attributes = createSpanAttributes('tool', {
-    tool_name: toolName,
-    ...toolAttributes,
-  })
-
-  const ctx = parentSpanCtx
-    ? trace.setSpan(otelContext.active(), parentSpanCtx.span)
-    : otelContext.active()
-  const span = tracer.startSpan('claude_code.tool', { attributes }, ctx)
-
-  // Add experimental tool input attributes
-  if (toolInput) {
-    addBetaToolInputAttributes(span, toolName, toolInput)
-  }
-
-  const spanId = getSpanId(span)
-  const spanContextObj: SpanContext = {
-    span,
-    startTime: Date.now(),
-    attributes,
-    perfettoSpanId,
-  }
-  activeSpans.set(spanId, new WeakRef(spanContextObj))
-
-  toolContext.enterWith(spanContextObj)
-
+  const span = createNoopSpan()
+  currentToolSpan = span
   return span
 }
 
 export function startToolBlockedOnUserSpan(): Span {
-  // Start Perfetto span regardless of OTel tracing state
-  const perfettoSpanId = isPerfettoTracingEnabled()
-    ? startUserInputPerfettoSpan('tool_permission')
-    : undefined
-
-  if (!isAnyTracingEnabled()) {
-    // Still track Perfetto span even if OTel is disabled
-    if (perfettoSpanId) {
-      const dummySpan = trace.getActiveSpan() || getTracer().startSpan('dummy')
-      const spanId = getSpanId(dummySpan)
-      const spanContextObj: SpanContext = {
-        span: dummySpan,
-        startTime: Date.now(),
-        attributes: { 'span.type': 'tool.blocked_on_user' },
-        perfettoSpanId,
-      }
-      activeSpans.set(spanId, new WeakRef(spanContextObj))
-      strongSpans.set(spanId, spanContextObj)
-      return dummySpan
-    }
-    return trace.getActiveSpan() || getTracer().startSpan('dummy')
-  }
-
-  const tracer = getTracer()
-  const parentSpanCtx = toolContext.getStore()
-
-  const attributes = createSpanAttributes('tool.blocked_on_user')
-
-  const ctx = parentSpanCtx
-    ? trace.setSpan(otelContext.active(), parentSpanCtx.span)
-    : otelContext.active()
-  const span = tracer.startSpan(
-    'claude_code.tool.blocked_on_user',
-    { attributes },
-    ctx,
-  )
-
-  const spanId = getSpanId(span)
-  const spanContextObj: SpanContext = {
-    span,
-    startTime: Date.now(),
-    attributes,
-    perfettoSpanId,
-  }
-  activeSpans.set(spanId, new WeakRef(spanContextObj))
-  strongSpans.set(spanId, spanContextObj)
-
-  return span
+  return createNoopSpan()
 }
 
 export function endToolBlockedOnUserSpan(
-  decision?: string,
-  source?: string,
-): void {
-  const blockedSpanContext = Array.from(activeSpans.values())
-    .findLast(
-      r => r.deref()?.attributes['span.type'] === 'tool.blocked_on_user',
-    )
-    ?.deref()
-
-  if (!blockedSpanContext) {
-    return
-  }
-
-  // End Perfetto span
-  if (blockedSpanContext.perfettoSpanId) {
-    endUserInputPerfettoSpan(blockedSpanContext.perfettoSpanId, {
-      decision,
-      source,
-    })
-  }
-
-  if (!isAnyTracingEnabled()) {
-    const spanId = getSpanId(blockedSpanContext.span)
-    activeSpans.delete(spanId)
-    strongSpans.delete(spanId)
-    return
-  }
-
-  const duration = Date.now() - blockedSpanContext.startTime
-  const attributes: Record<string, string | number | boolean> = {
-    duration_ms: duration,
-  }
-
-  if (decision) {
-    attributes['decision'] = decision
-  }
-  if (source) {
-    attributes['source'] = source
-  }
-
-  blockedSpanContext.span.setAttributes(attributes)
-  blockedSpanContext.span.end()
-
-  const spanId = getSpanId(blockedSpanContext.span)
-  activeSpans.delete(spanId)
-  strongSpans.delete(spanId)
-}
+  _decision?: string,
+  _source?: string,
+): void {}
 
 export function startToolExecutionSpan(): Span {
-  if (!isAnyTracingEnabled()) {
-    return trace.getActiveSpan() || getTracer().startSpan('dummy')
-  }
-
-  const tracer = getTracer()
-  const parentSpanCtx = toolContext.getStore()
-
-  const attributes = createSpanAttributes('tool.execution')
-
-  const ctx = parentSpanCtx
-    ? trace.setSpan(otelContext.active(), parentSpanCtx.span)
-    : otelContext.active()
-  const span = tracer.startSpan(
-    'claude_code.tool.execution',
-    { attributes },
-    ctx,
-  )
-
-  const spanId = getSpanId(span)
-  const spanContextObj: SpanContext = {
-    span,
-    startTime: Date.now(),
-    attributes,
-  }
-  activeSpans.set(spanId, new WeakRef(spanContextObj))
-  strongSpans.set(spanId, spanContextObj)
-
-  return span
+  return createNoopSpan()
 }
 
-export function endToolExecutionSpan(metadata?: {
+export function endToolExecutionSpan(_metadata?: {
   success?: boolean
   error?: string
-}): void {
-  if (!isAnyTracingEnabled()) {
-    return
-  }
-
-  const executionSpanContext = Array.from(activeSpans.values())
-    .findLast(r => r.deref()?.attributes['span.type'] === 'tool.execution')
-    ?.deref()
-
-  if (!executionSpanContext) {
-    return
-  }
-
-  const duration = Date.now() - executionSpanContext.startTime
-  const attributes: Record<string, string | number | boolean> = {
-    duration_ms: duration,
-  }
-
-  if (metadata) {
-    if (metadata.success !== undefined) attributes['success'] = metadata.success
-    if (metadata.error !== undefined) attributes['error'] = metadata.error
-  }
-
-  executionSpanContext.span.setAttributes(attributes)
-  executionSpanContext.span.end()
-
-  const spanId = getSpanId(executionSpanContext.span)
-  activeSpans.delete(spanId)
-  strongSpans.delete(spanId)
-}
-
-export function endToolSpan(toolResult?: string, resultTokens?: number): void {
-  const toolSpanContext = toolContext.getStore()
-
-  if (!toolSpanContext) {
-    return
-  }
-
-  // End Perfetto span
-  if (toolSpanContext.perfettoSpanId) {
-    endToolPerfettoSpan(toolSpanContext.perfettoSpanId, {
-      success: true,
-      resultTokens,
-    })
-  }
-
-  if (!isAnyTracingEnabled()) {
-    const spanId = getSpanId(toolSpanContext.span)
-    activeSpans.delete(spanId)
-    // Same reasoning as interactionContext above: clear so subsequent async
-    // work doesn't hold a stale reference to the ended tool span.
-    toolContext.enterWith(undefined)
-    return
-  }
-
-  const duration = Date.now() - toolSpanContext.startTime
-  const endAttributes: Record<string, string | number | boolean> = {
-    duration_ms: duration,
-  }
-
-  // Add experimental tool result attributes (new_context)
-  if (toolResult) {
-    const toolName = toolSpanContext.attributes['tool_name'] || 'unknown'
-    addBetaToolResultAttributes(endAttributes, toolName, toolResult)
-  }
-
-  if (resultTokens !== undefined) {
-    endAttributes['result_tokens'] = resultTokens
-  }
-
-  toolSpanContext.span.setAttributes(endAttributes)
-  toolSpanContext.span.end()
-
-  const spanId = getSpanId(toolSpanContext.span)
-  activeSpans.delete(spanId)
-  toolContext.enterWith(undefined)
-}
+}): void {}
 
-function isToolContentLoggingEnabled(): boolean {
-  return isEnvTruthy(process.env.OTEL_LOG_TOOL_CONTENT)
+export function endToolSpan(
+  _toolResult?: string,
+  _resultTokens?: number,
+): void {
+  currentToolSpan = null
 }
 
-/**
- * Add a span event with tool content/output data.
- * Only logs if OTEL_LOG_TOOL_CONTENT=1 is set.
- * Truncates content if it exceeds MAX_CONTENT_SIZE.
- */
 export function addToolContentEvent(
-  eventName: string,
-  attributes: Record<string, string | number | boolean>,
-): void {
-  if (!isAnyTracingEnabled() || !isToolContentLoggingEnabled()) {
-    return
-  }
-
-  const currentSpanCtx = toolContext.getStore()
-  if (!currentSpanCtx) {
-    return
-  }
-
-  // Truncate string attributes that might be large
-  const processedAttributes: Record<string, string | number | boolean> = {}
-  for (const [key, value] of Object.entries(attributes)) {
-    if (typeof value === 'string') {
-      const { content, truncated } = truncateContent(value)
-      processedAttributes[key] = content
-      if (truncated) {
-        processedAttributes[`${key}_truncated`] = true
-        processedAttributes[`${key}_original_length`] = value.length
-      }
-    } else {
-      processedAttributes[key] = value
-    }
-  }
-
-  currentSpanCtx.span.addEvent(eventName, processedAttributes)
-}
+  _eventName: string,
+  _attributes: SpanMetadata,
+): void {}
 
 export function getCurrentSpan(): Span | null {
-  if (!isAnyTracingEnabled()) {
-    return null
-  }
-
-  return (
-    toolContext.getStore()?.span ?? interactionContext.getStore()?.span ?? null
-  )
+  return currentToolSpan ?? currentInteractionSpan
 }
 
 export async function executeInSpan<T>(
-  spanName: string,
+  _spanName: string,
   fn: (span: Span) => Promise<T>,
-  attributes?: Record<string, string | number | boolean>,
+  _attributes?: SpanMetadata,
 ): Promise<T> {
-  if (!isAnyTracingEnabled()) {
-    return fn(trace.getActiveSpan() || getTracer().startSpan('dummy'))
-  }
-
-  const tracer = getTracer()
-  const parentSpanCtx = toolContext.getStore() ?? interactionContext.getStore()
-
-  const finalAttributes = createSpanAttributes('tool', {
-    ...attributes,
-  })
-
-  const ctx = parentSpanCtx
-    ? trace.setSpan(otelContext.active(), parentSpanCtx.span)
-    : otelContext.active()
-  const span = tracer.startSpan(spanName, { attributes: finalAttributes }, ctx)
-
-  const spanId = getSpanId(span)
-  const spanContextObj: SpanContext = {
-    span,
-    startTime: Date.now(),
-    attributes: finalAttributes,
-  }
-  activeSpans.set(spanId, new WeakRef(spanContextObj))
-  strongSpans.set(spanId, spanContextObj)
-
-  try {
-    const result = await fn(span)
-    span.end()
-    activeSpans.delete(spanId)
-    strongSpans.delete(spanId)
-    return result
-  } catch (error) {
-    if (error instanceof Error) {
-      span.recordException(error)
-    }
-    span.end()
-    activeSpans.delete(spanId)
-    strongSpans.delete(spanId)
-    throw error
-  }
+  return fn(createNoopSpan())
 }
 
-/**
- * Start a hook execution span.
- * Only creates a span when beta tracing is enabled.
- * @param hookEvent The hook event type (e.g., 'PreToolUse', 'PostToolUse')
- * @param hookName The full hook name (e.g., 'PreToolUse:Write')
- * @param numHooks The number of hooks being executed
- * @param hookDefinitions JSON string of hook definitions for tracing
- * @returns The span (or a dummy span if tracing is disabled)
- */
 export function startHookSpan(
-  hookEvent: string,
-  hookName: string,
-  numHooks: number,
-  hookDefinitions: string,
+  _hookEvent: string,
+  _hookName: string,
+  _numHooks: number,
+  _hookDefinitions: string,
 ): Span {
-  if (!isBetaTracingEnabled()) {
-    return trace.getActiveSpan() || getTracer().startSpan('dummy')
-  }
-
-  const tracer = getTracer()
-  const parentSpanCtx = toolContext.getStore() ?? interactionContext.getStore()
-
-  const attributes = createSpanAttributes('hook', {
-    hook_event: hookEvent,
-    hook_name: hookName,
-    num_hooks: numHooks,
-    hook_definitions: hookDefinitions,
-  })
-
-  const ctx = parentSpanCtx
-    ? trace.setSpan(otelContext.active(), parentSpanCtx.span)
-    : otelContext.active()
-  const span = tracer.startSpan('claude_code.hook', { attributes }, ctx)
-
-  const spanId = getSpanId(span)
-  const spanContextObj: SpanContext = {
-    span,
-    startTime: Date.now(),
-    attributes,
-  }
-  activeSpans.set(spanId, new WeakRef(spanContextObj))
-  strongSpans.set(spanId, spanContextObj)
-
-  return span
+  return createNoopSpan()
 }
 
-/**
- * End a hook execution span with outcome metadata.
- * Only does work when beta tracing is enabled.
- * @param span The span to end (returned from startHookSpan)
- * @param metadata The outcome metadata for the hook execution
- */
 export function endHookSpan(
-  span: Span,
-  metadata?: {
+  _span: Span,
+  _metadata?: {
     numSuccess?: number
     numBlocking?: number
     numNonBlockingError?: number
     numCancelled?: number
   },
-): void {
-  if (!isBetaTracingEnabled()) {
-    return
-  }
-
-  const spanId = getSpanId(span)
-  const spanContext = activeSpans.get(spanId)?.deref()
-
-  if (!spanContext) {
-    return
-  }
-
-  const duration = Date.now() - spanContext.startTime
-  const endAttributes: Record<string, string | number | boolean> = {
-    duration_ms: duration,
-  }
-
-  if (metadata) {
-    if (metadata.numSuccess !== undefined)
-      endAttributes['num_success'] = metadata.numSuccess
-    if (metadata.numBlocking !== undefined)
-      endAttributes['num_blocking'] = metadata.numBlocking
-    if (metadata.numNonBlockingError !== undefined)
-      endAttributes['num_non_blocking_error'] = metadata.numNonBlockingError
-    if (metadata.numCancelled !== undefined)
-      endAttributes['num_cancelled'] = metadata.numCancelled
-  }
-
-  spanContext.span.setAttributes(endAttributes)
-  spanContext.span.end()
-  activeSpans.delete(spanId)
-  strongSpans.delete(spanId)
-}
+): void {}