chore: restore local telemetry infrastructure
This commit is contained in:
parent
af7af8cc9d
commit
83abbbc2fe
|
|
@ -16,10 +16,8 @@ import { getEventMetadata } from './metadata.js'
|
||||||
* DATADOG_LOGS_ENDPOINT=https://http-intake.logs.datadoghq.com/api/v2/logs
|
* DATADOG_LOGS_ENDPOINT=https://http-intake.logs.datadoghq.com/api/v2/logs
|
||||||
* DATADOG_API_KEY=<your-key>
|
* DATADOG_API_KEY=<your-key>
|
||||||
*/
|
*/
|
||||||
const DATADOG_LOGS_ENDPOINT =
|
const DATADOG_LOGS_ENDPOINT = process.env.DATADOG_LOGS_ENDPOINT ?? ''
|
||||||
process.env.DATADOG_LOGS_ENDPOINT ?? ''
|
const DATADOG_CLIENT_TOKEN = process.env.DATADOG_API_KEY ?? ''
|
||||||
const DATADOG_CLIENT_TOKEN =
|
|
||||||
process.env.DATADOG_API_KEY ?? ''
|
|
||||||
const DEFAULT_FLUSH_INTERVAL_MS = 15000
|
const DEFAULT_FLUSH_INTERVAL_MS = 15000
|
||||||
const MAX_BATCH_SIZE = 100
|
const MAX_BATCH_SIZE = 100
|
||||||
const NETWORK_TIMEOUT_MS = 5000
|
const NETWORK_TIMEOUT_MS = 5000
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import { getPlatform, getWslVersion } from '../../utils/platform.js'
|
||||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||||
import { profileCheckpoint } from '../../utils/startupProfiler.js'
|
import { profileCheckpoint } from '../../utils/startupProfiler.js'
|
||||||
import { getCoreUserData } from '../../utils/user.js'
|
import { getCoreUserData } from '../../utils/user.js'
|
||||||
import { isAnalyticsDisabled } from './config.js'
|
|
||||||
import { FirstPartyEventLoggingExporter } from './firstPartyEventLoggingExporter.js'
|
import { FirstPartyEventLoggingExporter } from './firstPartyEventLoggingExporter.js'
|
||||||
import type { GrowthBookUserAttributes } from './growthbook.js'
|
import type { GrowthBookUserAttributes } from './growthbook.js'
|
||||||
import { getDynamicConfig_CACHED_MAY_BE_STALE } from './growthbook.js'
|
import { getDynamicConfig_CACHED_MAY_BE_STALE } from './growthbook.js'
|
||||||
|
|
@ -139,8 +138,10 @@ export async function shutdown1PEventLogging(): Promise<void> {
|
||||||
* metrics opt-out via API. It follows the same pattern as Statsig event logging.
|
* metrics opt-out via API. It follows the same pattern as Statsig event logging.
|
||||||
*/
|
*/
|
||||||
export function is1PEventLoggingEnabled(): boolean {
|
export function is1PEventLoggingEnabled(): boolean {
|
||||||
// Respect standard analytics opt-outs
|
// CCP keeps analytics local by default. The restored upstream 1P pipeline
|
||||||
return !isAnalyticsDisabled()
|
// remains available in the source tree, but is not allowed to send telemetry
|
||||||
|
// to Anthropic from this fork.
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -331,6 +332,7 @@ export function initialize1PEventLogging(): void {
|
||||||
parseInt(
|
parseInt(
|
||||||
process.env.OTEL_LOGS_EXPORT_INTERVAL ||
|
process.env.OTEL_LOGS_EXPORT_INTERVAL ||
|
||||||
DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(),
|
DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(),
|
||||||
|
10,
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxExportBatchSize =
|
const maxExportBatchSize =
|
||||||
|
|
|
||||||
|
|
@ -109,15 +109,13 @@ export class FirstPartyEventLoggingExporter implements LogRecordExporter {
|
||||||
schedule?: (fn: () => Promise<void>, delayMs: number) => () => void
|
schedule?: (fn: () => Promise<void>, delayMs: number) => () => void
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
// Default: prod, except when ANTHROPIC_BASE_URL is explicitly staging.
|
// CCP has no default remote endpoint. A caller must explicitly provide a
|
||||||
// Overridable via tengu_1p_event_batch_config.baseUrl.
|
// baseUrl to export 1P events off-machine.
|
||||||
const baseUrl =
|
const baseUrl = options.baseUrl
|
||||||
options.baseUrl ||
|
|
||||||
(process.env.ANTHROPIC_BASE_URL === 'https://api-staging.anthropic.com'
|
|
||||||
? 'https://api-staging.anthropic.com'
|
|
||||||
: 'https://api.anthropic.com')
|
|
||||||
|
|
||||||
this.endpoint = `${baseUrl}${options.path || '/api/event_logging/batch'}`
|
this.endpoint = baseUrl
|
||||||
|
? `${baseUrl}${options.path || '/api/event_logging/batch'}`
|
||||||
|
: ''
|
||||||
|
|
||||||
this.timeout = options.timeout || 10000
|
this.timeout = options.timeout || 10000
|
||||||
this.maxBatchSize = options.maxBatchSize || 200
|
this.maxBatchSize = options.maxBatchSize || 200
|
||||||
|
|
@ -135,7 +133,9 @@ export class FirstPartyEventLoggingExporter implements LogRecordExporter {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Retry any failed events from previous runs of this session (in background)
|
// Retry any failed events from previous runs of this session (in background)
|
||||||
void this.retryPreviousBatches()
|
if (this.endpoint) {
|
||||||
|
void this.retryPreviousBatches()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose for testing
|
// Expose for testing
|
||||||
|
|
@ -307,6 +307,11 @@ export class FirstPartyEventLoggingExporter implements LogRecordExporter {
|
||||||
logs: ReadableLogRecord[],
|
logs: ReadableLogRecord[],
|
||||||
resultCallback: (result: ExportResult) => void,
|
resultCallback: (result: ExportResult) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (!this.endpoint) {
|
||||||
|
resultCallback({ code: ExportResultCode.SUCCESS })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Filter for event logs only (by scope name)
|
// Filter for event logs only (by scope name)
|
||||||
const eventLogs = logs.filter(
|
const eventLogs = logs.filter(
|
||||||
|
|
@ -673,7 +678,9 @@ export class FirstPartyEventLoggingExporter implements LogRecordExporter {
|
||||||
(attributes.event_name as string) || (log.body as string) || 'unknown'
|
(attributes.event_name as string) || (log.body as string) || 'unknown'
|
||||||
|
|
||||||
// Extract metadata objects directly (no JSON parsing needed)
|
// Extract metadata objects directly (no JSON parsing needed)
|
||||||
const coreMetadata = attributes.core_metadata as unknown as EventMetadata | undefined
|
const coreMetadata = attributes.core_metadata as unknown as
|
||||||
|
| EventMetadata
|
||||||
|
| undefined
|
||||||
const userMetadata = attributes.user_metadata as CoreUserData
|
const userMetadata = attributes.user_metadata as CoreUserData
|
||||||
const eventMetadata = (attributes.event_metadata || {}) as Record<
|
const eventMetadata = (attributes.event_metadata || {}) as Record<
|
||||||
string,
|
string,
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,9 @@ import {
|
||||||
type GitHubActionsMetadata,
|
type GitHubActionsMetadata,
|
||||||
getUserForGrowthBook,
|
getUserForGrowthBook,
|
||||||
} from '../../utils/user.js'
|
} from '../../utils/user.js'
|
||||||
import {
|
import { logGrowthBookExperimentTo1P } from './firstPartyEventLogger.js'
|
||||||
is1PEventLoggingEnabled,
|
|
||||||
logGrowthBookExperimentTo1P,
|
const DEFAULT_API_HOST = 'api.anthropic.com' // core API host, not a telemetry endpoint
|
||||||
} from './firstPartyEventLogger.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User attributes sent to GrowthBook for targeting.
|
* User attributes sent to GrowthBook for targeting.
|
||||||
|
|
@ -466,9 +465,14 @@ const LOCAL_GATE_DEFAULTS: Record<string, unknown> = {
|
||||||
tengu_birch_trellis: true, // Tree-sitter bash security analysis
|
tengu_birch_trellis: true, // Tree-sitter bash security analysis
|
||||||
tengu_collage_kaleidoscope: true, // macOS clipboard image reading
|
tengu_collage_kaleidoscope: true, // macOS clipboard image reading
|
||||||
tengu_compact_cache_prefix: true, // Reuse prompt cache during compaction
|
tengu_compact_cache_prefix: true, // Reuse prompt cache during compaction
|
||||||
|
tengu_kairos_assistant: true, // KAIROS assistant mode activation
|
||||||
tengu_kairos_cron_durable: true, // Persistent cron tasks
|
tengu_kairos_cron_durable: true, // Persistent cron tasks
|
||||||
tengu_attribution_header: true, // API request attribution header
|
tengu_attribution_header: true, // API request attribution header
|
||||||
tengu_slate_prism: true, // Agent progress summaries
|
tengu_slate_prism: true, // Agent progress summaries
|
||||||
|
|
||||||
|
// ── Ultrareview (cloud code review via CCR) ─────────────────────
|
||||||
|
tengu_review_bughunter_config: { enabled: true }, // /ultrareview command visibility
|
||||||
|
tengu_ccr_bundle_seed_enabled: true, // Bundle seed: skip GitHub App check for branch mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -486,12 +490,13 @@ function getLocalGateDefault(feature: string): unknown | undefined {
|
||||||
* Check if GrowthBook operations should be enabled
|
* Check if GrowthBook operations should be enabled
|
||||||
*/
|
*/
|
||||||
function isGrowthBookEnabled(): boolean {
|
function isGrowthBookEnabled(): boolean {
|
||||||
// 适配器模式:有自定义服务器配置时直接启用
|
// Adapter mode is the only network-enabled mode in CCP. Without an explicit
|
||||||
|
// custom GrowthBook adapter, feature values come from env/config overrides
|
||||||
|
// and LOCAL_GATE_DEFAULTS only.
|
||||||
if (process.env.CLAUDE_GB_ADAPTER_URL && process.env.CLAUDE_GB_ADAPTER_KEY) {
|
if (process.env.CLAUDE_GB_ADAPTER_URL && process.env.CLAUDE_GB_ADAPTER_KEY) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// GrowthBook depends on 1P event logging.
|
return false
|
||||||
return is1PEventLoggingEnabled()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -503,15 +508,15 @@ function isGrowthBookEnabled(): boolean {
|
||||||
* attributes. Without this, there's no stable attribute to target them on
|
* attributes. Without this, there's no stable attribute to target them on
|
||||||
* — only per-device IDs. See src/utils/auth.ts isAnthropicAuthEnabled().
|
* — only per-device IDs. See src/utils/auth.ts isAnthropicAuthEnabled().
|
||||||
*
|
*
|
||||||
* Returns undefined for unset/default (api.anthropic.com) so the attribute
|
* Returns undefined for unset/default Anthropic API host so the attribute is
|
||||||
* is absent for direct-API users. Hostname only — no path/query/creds.
|
* absent for direct-API users. Hostname only — no path/query/creds.
|
||||||
*/
|
*/
|
||||||
export function getApiBaseUrlHost(): string | undefined {
|
export function getApiBaseUrlHost(): string | undefined {
|
||||||
const baseUrl = process.env.ANTHROPIC_BASE_URL
|
const baseUrl = process.env.ANTHROPIC_BASE_URL
|
||||||
if (!baseUrl) return undefined
|
if (!baseUrl) return undefined
|
||||||
try {
|
try {
|
||||||
const host = new URL(baseUrl).host
|
const host = new URL(baseUrl).host
|
||||||
if (host === 'api.anthropic.com') return undefined
|
if (host === DEFAULT_API_HOST) return undefined
|
||||||
return host
|
return host
|
||||||
} catch {
|
} catch {
|
||||||
return undefined
|
return undefined
|
||||||
|
|
@ -565,11 +570,7 @@ const getGrowthBookClient = memoize(
|
||||||
|
|
||||||
const attributes = getUserAttributes()
|
const attributes = getUserAttributes()
|
||||||
const clientKey = getGrowthBookClientKey()
|
const clientKey = getGrowthBookClientKey()
|
||||||
const baseUrl =
|
const baseUrl = process.env.CLAUDE_GB_ADAPTER_URL
|
||||||
process.env.CLAUDE_GB_ADAPTER_URL ||
|
|
||||||
(process.env.USER_TYPE === 'ant'
|
|
||||||
? process.env.CLAUDE_CODE_GB_BASE_URL || 'https://api.anthropic.com/'
|
|
||||||
: 'https://api.anthropic.com/')
|
|
||||||
const isAdapterMode = !!(
|
const isAdapterMode = !!(
|
||||||
process.env.CLAUDE_GB_ADAPTER_URL && process.env.CLAUDE_GB_ADAPTER_KEY
|
process.env.CLAUDE_GB_ADAPTER_URL && process.env.CLAUDE_GB_ADAPTER_KEY
|
||||||
)
|
)
|
||||||
|
|
@ -830,6 +831,16 @@ export function getFeatureValue_CACHED_MAY_BE_STALE<T>(
|
||||||
return localDefault !== undefined ? (localDefault as T) : defaultValue
|
return localDefault !== undefined ? (localDefault as T) : defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LOCAL_GATE_DEFAULTS take priority over remote values and disk cache.
|
||||||
|
// In fork/self-hosted deployments, the GrowthBook server may push false
|
||||||
|
// for gates we intentionally enable. Local defaults represent the
|
||||||
|
// project's intentional configuration and override everything except
|
||||||
|
// env/config overrides (which are explicit user intent).
|
||||||
|
const localDefault = getLocalGateDefault(feature)
|
||||||
|
if (localDefault !== undefined) {
|
||||||
|
return localDefault as T
|
||||||
|
}
|
||||||
|
|
||||||
// Log experiment exposure if data is available, otherwise defer until after init
|
// Log experiment exposure if data is available, otherwise defer until after init
|
||||||
if (experimentDataByFeature.has(feature)) {
|
if (experimentDataByFeature.has(feature)) {
|
||||||
logExposureForFeature(feature)
|
logExposureForFeature(feature)
|
||||||
|
|
@ -838,10 +849,6 @@ export function getFeatureValue_CACHED_MAY_BE_STALE<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-memory payload is authoritative once processRemoteEvalPayload has run.
|
// In-memory payload is authoritative once processRemoteEvalPayload has run.
|
||||||
// Disk is also fresh by then (syncRemoteEvalToDisk runs synchronously inside
|
|
||||||
// init), so this is correctness-equivalent to the disk read below — but it
|
|
||||||
// skips the config JSON parse and is what onGrowthBookRefresh subscribers
|
|
||||||
// depend on to read fresh values the instant they're notified.
|
|
||||||
if (remoteEvalFeatureValues.has(feature)) {
|
if (remoteEvalFeatureValues.has(feature)) {
|
||||||
return remoteEvalFeatureValues.get(feature) as T
|
return remoteEvalFeatureValues.get(feature) as T
|
||||||
}
|
}
|
||||||
|
|
@ -853,14 +860,9 @@ export function getFeatureValue_CACHED_MAY_BE_STALE<T>(
|
||||||
return cached as T
|
return cached as T
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Config not yet initialized — fall through to local gate defaults
|
// Config not yet initialized — fall through to defaultValue
|
||||||
}
|
}
|
||||||
// Disk cache miss (or config not initialized) — use local gate defaults
|
return defaultValue
|
||||||
// before falling back to the caller's defaultValue. This covers:
|
|
||||||
// 1. GrowthBook "enabled" but never connected (caches empty)
|
|
||||||
// 2. Config not yet initialized (early in startup)
|
|
||||||
const localDefault = getLocalGateDefault(feature)
|
|
||||||
return localDefault !== undefined ? (localDefault as T) : defaultValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,17 @@ type QueuedEvent = {
|
||||||
async: boolean
|
async: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeLocalAnalyticsEvent(
|
||||||
|
eventName: string,
|
||||||
|
metadata: LogEventMetadata,
|
||||||
|
): void {
|
||||||
|
// Local analytics stays independent of the optional upstream sinks.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const { writeLocalEvent } =
|
||||||
|
require('./localSink.js') as typeof import('./localSink.js')
|
||||||
|
writeLocalEvent(eventName, metadata as Record<string, unknown>)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sink interface for the analytics backend
|
* Sink interface for the analytics backend
|
||||||
*/
|
*/
|
||||||
|
|
@ -136,13 +147,7 @@ export function logEvent(
|
||||||
// to avoid accidentally logging code/filepaths
|
// to avoid accidentally logging code/filepaths
|
||||||
metadata: LogEventMetadata,
|
metadata: LogEventMetadata,
|
||||||
): void {
|
): void {
|
||||||
// Local analytics: write EVERY event to a local JSONL file for self-analysis.
|
writeLocalAnalyticsEvent(eventName, metadata)
|
||||||
// Runs before the upstream sink so local data is never lost even if the
|
|
||||||
// upstream sink throws. Import is inline to avoid adding a module-level
|
|
||||||
// dependency to this zero-dependency entry point.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
||||||
const { writeLocalEvent } = require('./localSink.js') as typeof import('./localSink.js')
|
|
||||||
writeLocalEvent(eventName, metadata as Record<string, unknown>)
|
|
||||||
|
|
||||||
if (sink === null) {
|
if (sink === null) {
|
||||||
eventQueue.push({ eventName, metadata, async: false })
|
eventQueue.push({ eventName, metadata, async: false })
|
||||||
|
|
@ -164,6 +169,8 @@ export async function logEventAsync(
|
||||||
// intentionally no strings, to avoid accidentally logging code/filepaths
|
// intentionally no strings, to avoid accidentally logging code/filepaths
|
||||||
metadata: LogEventMetadata,
|
metadata: LogEventMetadata,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
writeLocalAnalyticsEvent(eventName, metadata)
|
||||||
|
|
||||||
if (sink === null) {
|
if (sink === null) {
|
||||||
eventQueue.push({ eventName, metadata, async: true })
|
eventQueue.push({ eventName, metadata, async: true })
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ import {
|
||||||
isTeammate,
|
isTeammate,
|
||||||
} from '../../utils/teammate.js'
|
} from '../../utils/teammate.js'
|
||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import { COMPUTER_USE_MCP_SERVER_NAME } from '../../utils/computerUse/common.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker type for verifying analytics metadata doesn't contain sensitive data
|
* Marker type for verifying analytics metadata doesn't contain sensitive data
|
||||||
|
|
@ -126,9 +125,17 @@ export function isAnalyticsToolDetailsLoggingEnabled(
|
||||||
* reservation (main.tsx, config.ts addMcpServer) is itself feature-gated, so
|
* reservation (main.tsx, config.ts addMcpServer) is itself feature-gated, so
|
||||||
* a user-configured 'computer-use' is possible in builds without the feature.
|
* a user-configured 'computer-use' is possible in builds without the feature.
|
||||||
*/
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const BUILTIN_MCP_SERVER_NAMES: ReadonlySet<string> = new Set(
|
const BUILTIN_MCP_SERVER_NAMES: ReadonlySet<string> = new Set(
|
||||||
feature('CHICAGO_MCP') ? [COMPUTER_USE_MCP_SERVER_NAME] : [],
|
feature('CHICAGO_MCP')
|
||||||
|
? [
|
||||||
|
(
|
||||||
|
require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
|
||||||
|
).COMPUTER_USE_MCP_SERVER_NAME,
|
||||||
|
]
|
||||||
|
: [],
|
||||||
)
|
)
|
||||||
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spreadable helper for logEvent payloads — returns {mcpServerName, mcpToolName}
|
* Spreadable helper for logEvent payloads — returns {mcpServerName, mcpToolName}
|
||||||
|
|
@ -735,7 +742,6 @@ export async function getEventMetadata(
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core event metadata for 1P event logging (snake_case format).
|
* Core event metadata for 1P event logging (snake_case format).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ type LogEventMetadata = { [key: string]: boolean | number | undefined }
|
||||||
const DATADOG_GATE_NAME = 'tengu_log_datadog_events'
|
const DATADOG_GATE_NAME = 'tengu_log_datadog_events'
|
||||||
|
|
||||||
// Module-level gate state - starts undefined, initialized during startup
|
// Module-level gate state - starts undefined, initialized during startup
|
||||||
let isDatadogGateEnabled: boolean | undefined = undefined
|
let isDatadogGateEnabled: boolean | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Datadog tracking is enabled.
|
* Check if Datadog tracking is enabled.
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,12 @@ async function _fetchMetricsEnabled(): Promise<MetricsEnabledResponse> {
|
||||||
...authResult.headers,
|
...authResult.headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = `https://api.anthropic.com/api/claude_code/organizations/metrics_enabled`
|
const metricsBaseUrl = process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT
|
||||||
|
if (!metricsBaseUrl) {
|
||||||
|
throw new Error('Metrics endpoint is not configured')
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = `${metricsBaseUrl}/api/claude_code/organizations/metrics_enabled`
|
||||||
const response = await axios.get<MetricsEnabledResponse>(endpoint, {
|
const response = await axios.get<MetricsEnabledResponse>(endpoint, {
|
||||||
headers,
|
headers,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import { isEnvTruthy } from './envUtils.js'
|
||||||
import { toError, shortErrorStack } from './errors.js'
|
import { toError, shortErrorStack } from './errors.js'
|
||||||
import { isEssentialTrafficOnly } from './privacyLevel.js'
|
import { isEssentialTrafficOnly } from './privacyLevel.js'
|
||||||
import { jsonParse } from './slowOperations.js'
|
import { jsonParse } from './slowOperations.js'
|
||||||
import { redactForLog } from './sensitive.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the display title for a log/session with fallback logic.
|
* Gets the display title for a log/session with fallback logic.
|
||||||
|
|
@ -159,11 +158,7 @@ const isHardFailMode = memoize((): boolean => {
|
||||||
export function logError(error: unknown): void {
|
export function logError(error: unknown): void {
|
||||||
const err = toError(error)
|
const err = toError(error)
|
||||||
if (feature('HARD_FAIL') && isHardFailMode()) {
|
if (feature('HARD_FAIL') && isHardFailMode()) {
|
||||||
// biome-ignore lint/suspicious/noConsole:: intentional crash output
|
console.error('[HARD FAIL] logError called with:', err.stack || err.message)
|
||||||
console.error(
|
|
||||||
'[HARD FAIL] logError called with:',
|
|
||||||
redactForLog(err.stack || err.message),
|
|
||||||
)
|
|
||||||
// eslint-disable-next-line custom-rules/no-process-exit
|
// eslint-disable-next-line custom-rules/no-process-exit
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +230,7 @@ export async function getErrorLogByIndex(
|
||||||
async function loadLogList(path: string): Promise<LogOption[]> {
|
async function loadLogList(path: string): Promise<LogOption[]> {
|
||||||
let files: Awaited<ReturnType<typeof readdir>>
|
let files: Awaited<ReturnType<typeof readdir>>
|
||||||
try {
|
try {
|
||||||
files = await readdir(path, { withFileTypes: true }) as any
|
files = (await readdir(path, { withFileTypes: true })) as any
|
||||||
} catch {
|
} catch {
|
||||||
logError(new Error(`No logs found at ${path}`))
|
logError(new Error(`No logs found at ${path}`))
|
||||||
return []
|
return []
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,9 @@ const shim = {
|
||||||
clearResourceTimings: (() => {}) as typeof performance.clearResourceTimings,
|
clearResourceTimings: (() => {}) as typeof performance.clearResourceTimings,
|
||||||
setResourceTimingBufferSize:
|
setResourceTimingBufferSize:
|
||||||
(() => {}) as typeof performance.setResourceTimingBufferSize,
|
(() => {}) as typeof performance.setResourceTimingBufferSize,
|
||||||
|
// Node.js v22 undici internal calls this after every fetch — must exist to
|
||||||
|
// avoid TypeError: markResourceTiming is not a function
|
||||||
|
markResourceTiming: (() => {}) as any,
|
||||||
// Delegate read-only properties to the original
|
// Delegate read-only properties to the original
|
||||||
get timeOrigin() {
|
get timeOrigin() {
|
||||||
return original.timeOrigin
|
return original.timeOrigin
|
||||||
|
|
@ -148,7 +151,7 @@ const shim = {
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return original.toJSON()
|
return original.toJSON()
|
||||||
},
|
},
|
||||||
} as typeof performance
|
} as unknown as typeof performance
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install the shim onto globalThis.performance. Safe to call multiple times.
|
* Install the shim onto globalThis.performance. Safe to call multiple times.
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,13 @@ export function profileReport(): void {
|
||||||
logForDebugging('Startup profiling report:')
|
logForDebugging('Startup profiling report:')
|
||||||
logForDebugging(getReport())
|
logForDebugging(getReport())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear startup marks to prevent PerformanceMark accumulation in long-lived
|
||||||
|
// processes (daemon, cron). After this point startup marks are no longer needed
|
||||||
|
// — the report has been written and the Statsig event has been logged.
|
||||||
|
const perf = getPerformance()
|
||||||
|
perf.clearMarks()
|
||||||
|
memorySnapshots.length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDetailedProfilingEnabled(): boolean {
|
export function isDetailedProfilingEnabled(): boolean {
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,6 @@ export class BigQueryMetricsExporter implements PushMetricExporter {
|
||||||
private isShutdown = false
|
private isShutdown = false
|
||||||
|
|
||||||
constructor(options: { timeout?: number } = {}) {
|
constructor(options: { timeout?: number } = {}) {
|
||||||
const defaultEndpoint = 'https://api.anthropic.com/api/claude_code/metrics'
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.USER_TYPE === 'ant' &&
|
process.env.USER_TYPE === 'ant' &&
|
||||||
process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT
|
process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT
|
||||||
|
|
@ -54,7 +52,7 @@ export class BigQueryMetricsExporter implements PushMetricExporter {
|
||||||
process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT +
|
process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT +
|
||||||
'/api/claude_code/metrics'
|
'/api/claude_code/metrics'
|
||||||
} else {
|
} else {
|
||||||
this.endpoint = defaultEndpoint
|
this.endpoint = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timeout = options.timeout || 5000
|
this.timeout = options.timeout || 5000
|
||||||
|
|
@ -88,6 +86,11 @@ export class BigQueryMetricsExporter implements PushMetricExporter {
|
||||||
metrics: ResourceMetrics,
|
metrics: ResourceMetrics,
|
||||||
resultCallback: (result: ExportResult) => void,
|
resultCallback: (result: ExportResult) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (!this.endpoint) {
|
||||||
|
resultCallback({ code: ExportResultCode.SUCCESS })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Skip if trust not established in interactive mode
|
// Skip if trust not established in interactive mode
|
||||||
// This prevents triggering apiKeyHelper before trust dialog
|
// This prevents triggering apiKeyHelper before trust dialog
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ async function getOtlpReaders() {
|
||||||
}
|
}
|
||||||
case 'http/json': {
|
case 'http/json': {
|
||||||
const { OTLPMetricExporter } = await import(
|
const { OTLPMetricExporter } = await import(
|
||||||
'@opentelemetry/exporter-metrics-otlp-http'
|
'@opentelemetry/exporter-metrics-otlp-http' // package name, not endpoint
|
||||||
)
|
)
|
||||||
exporters.push(new OTLPMetricExporter(httpConfig))
|
exporters.push(new OTLPMetricExporter(httpConfig))
|
||||||
break
|
break
|
||||||
|
|
@ -282,7 +282,7 @@ async function getOtlpLogExporters() {
|
||||||
}
|
}
|
||||||
case 'http/json': {
|
case 'http/json': {
|
||||||
const { OTLPLogExporter } = await import(
|
const { OTLPLogExporter } = await import(
|
||||||
'@opentelemetry/exporter-logs-otlp-http'
|
'@opentelemetry/exporter-logs-otlp-http' // package name, not endpoint
|
||||||
)
|
)
|
||||||
exporters.push(new OTLPLogExporter(httpConfig))
|
exporters.push(new OTLPLogExporter(httpConfig))
|
||||||
break
|
break
|
||||||
|
|
@ -333,7 +333,7 @@ async function getOtlpTraceExporters() {
|
||||||
}
|
}
|
||||||
case 'http/json': {
|
case 'http/json': {
|
||||||
const { OTLPTraceExporter } = await import(
|
const { OTLPTraceExporter } = await import(
|
||||||
'@opentelemetry/exporter-trace-otlp-http'
|
'@opentelemetry/exporter-trace-otlp-http' // package name, not endpoint
|
||||||
)
|
)
|
||||||
exporters.push(new OTLPTraceExporter(httpConfig))
|
exporters.push(new OTLPTraceExporter(httpConfig))
|
||||||
break
|
break
|
||||||
|
|
@ -373,6 +373,10 @@ function getBigQueryExportingReader() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBigQueryMetricsEnabled() {
|
function isBigQueryMetricsEnabled() {
|
||||||
|
if (!process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// BigQuery metrics are enabled for:
|
// BigQuery metrics are enabled for:
|
||||||
// 1. API customers (excluding Claude.ai subscribers and Bedrock/Vertex)
|
// 1. API customers (excluding Claude.ai subscribers and Bedrock/Vertex)
|
||||||
// 2. Claude for Enterprise (C4E) users
|
// 2. Claude for Enterprise (C4E) users
|
||||||
|
|
@ -398,8 +402,8 @@ async function initializeBetaTracing(
|
||||||
}
|
}
|
||||||
|
|
||||||
const [{ OTLPTraceExporter }, { OTLPLogExporter }] = await Promise.all([
|
const [{ OTLPTraceExporter }, { OTLPLogExporter }] = await Promise.all([
|
||||||
import('@opentelemetry/exporter-trace-otlp-http'),
|
import('@opentelemetry/exporter-trace-otlp-http'), // package name, not endpoint
|
||||||
import('@opentelemetry/exporter-logs-otlp-http'),
|
import('@opentelemetry/exporter-logs-otlp-http'), // package name, not endpoint
|
||||||
])
|
])
|
||||||
|
|
||||||
const httpConfig = {
|
const httpConfig = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user