surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
| 1 | import type { HttpResponse, Timer } from 'claude-code' |
| 2 | |
| 3 | import type { Telemetry } from '../../types' |
| 4 | import Batching from '../batching' |
| 5 | import Context from '../context' |
| 6 | import Entries from '../entries' |
| 7 | import type { Environment } from '../environment' |
| 8 | import { isAnalyticsOff } from '../is-analytics-off' |
| 9 | import type { Sender } from '../sender' |
| 10 | import type { TelemetryDeps } from '../telemetry-deps' |
| 11 | |
| 12 | /** |
| 13 | * Builds `$.telemetry` and its flush: `log` and `mark` check the entry and |
| 14 | * queue a row; a batch goes out on a timer, when full, or when flushed. |
| 15 | * |
| 16 | * Each batch reads the switches afresh: analytics off, or unreadable, sends |
| 17 | * nothing; no credential, or an ingest that refuses after one retry, drops |
| 18 | * the batch. Each outcome is one debug line; the context is gathered once. |
| 19 | * |
| 20 | * @param deps the calls on the nouns beneath |
| 21 | * @returns the noun and its flush |
| 22 | */ |
| 23 | export function telemetryOf(deps: TelemetryDeps): Sender { |
| 24 | let pending: Batching.PendingRow[] = [] |
| 25 | let timer: Timer | undefined |
| 26 | let context: Promise<Context.Context> | undefined |
| 27 | let priming: Promise<void> | undefined |
| 28 | let sending: Promise<void> = Promise.resolve() |
| 29 | |
| 30 | function gathered(): Promise<Context.Context> { |
| 31 | const gathering = |
| 32 | context ?? |
| 33 | deps |
| 34 | .isInteractive() |
| 35 | .then(isInteractive => Context.contextOf(deps, isInteractive)) |
| 36 | .catch((error: unknown) => { |
| 37 | context = undefined |
| 38 | throw error |
| 39 | }) |
| 40 | |
| 41 | context = gathering |
| 42 | |
| 43 | return gathering |
| 44 | } |
| 45 | |
| 46 | async function sendingEnvironment(): Promise<Environment | undefined> { |
| 47 | try { |
| 48 | const [environment, policy] = await Promise.all([ |
| 49 | deps.environment(), |
| 50 | deps.policy(), |
| 51 | ]) |
| 52 | |
| 53 | return isAnalyticsOff(environment, policy) ? undefined : environment |
| 54 | } catch (error) { |
| 55 | deps.debug( |
| 56 | 'telemetry: the analytics switches could not be read, so nothing ' + |
| 57 | `is sent (${Batching.messageOf(error)})`, |
| 58 | ) |
| 59 | |
| 60 | return undefined |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | async function primed(): Promise<void> { |
| 65 | const environment = await sendingEnvironment() |
| 66 | |
| 67 | if (environment) { |
| 68 | await gathered() |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | const post = (body: string, auth: string): Promise<HttpResponse> => |
| 73 | deps.fetch(Entries.INGEST_URL, { |
| 74 | method: 'POST', |
| 75 | headers: { |
| 76 | 'Content-Type': 'application/json', |
| 77 | 'x-service-name': 'claude-code', |
| 78 | }, |
| 79 | auth, |
| 80 | body, |
| 81 | }) |
| 82 | |
| 83 | async function postedWithRetry( |
| 84 | body: string, |
| 85 | auth: string, |
| 86 | ): Promise<HttpResponse> { |
| 87 | let first: HttpResponse | undefined |
| 88 | |
| 89 | try { |
| 90 | first = await post(body, auth) |
| 91 | } catch { |
| 92 | first = undefined |
| 93 | } |
| 94 | |
| 95 | const isSettled = |
| 96 | first !== undefined && (first.ok || !Batching.isRetriable(first.status)) |
| 97 | |
| 98 | if (first !== undefined && isSettled) { |
| 99 | return first |
| 100 | } |
| 101 | |
| 102 | await deps.sleep(Batching.RETRY_DELAY_MS) |
| 103 | |
| 104 | return post(body, auth) |
| 105 | } |
| 106 | |
| 107 | async function send(rows: readonly Batching.PendingRow[]): Promise<void> { |
| 108 | const environment = await sendingEnvironment() |
| 109 | |
| 110 | if (!environment) { |
| 111 | return |
| 112 | } |
| 113 | |
| 114 | const authorization = await deps.authorize() |
| 115 | |
| 116 | if (!authorization) { |
| 117 | throw new Error('this session has no first-party credential to authorize') |
| 118 | } |
| 119 | |
| 120 | const settled = await gathered() |
| 121 | |
| 122 | const response = await postedWithRetry( |
| 123 | Entries.batchOf(rows, { |
| 124 | sessionId: await deps.id(), |
| 125 | model: await deps.model(), |
| 126 | userType: environment.userType === 'ant' ? 'ant' : 'external', |
| 127 | isInteractive: await deps.isInteractive(), |
| 128 | isClaudeAiAuth: |
| 129 | authorization.kind === 'bearer' && |
| 130 | settled.identity.accountUuid !== undefined, |
| 131 | context: settled, |
| 132 | }), |
| 133 | authorization.handle, |
| 134 | ) |
| 135 | |
| 136 | if (!response.ok) { |
| 137 | throw new Error(`the ingest answered ${response.status}`) |
| 138 | } |
| 139 | |
| 140 | deps.debug(`telemetry: sent ${rows.length} row(s)`) |
| 141 | } |
| 142 | |
| 143 | function flush(): Promise<void> { |
| 144 | timer?.cancel() |
| 145 | timer = undefined |
| 146 | |
| 147 | const rows = pending |
| 148 | |
| 149 | pending = [] |
| 150 | |
| 151 | if (rows.length === 0) { |
| 152 | return sending |
| 153 | } |
| 154 | |
| 155 | sending = sending.then(() => |
| 156 | send(rows).catch((error: unknown) => { |
| 157 | deps.debug( |
| 158 | `telemetry: ${rows.length} row(s) not sent: ` + |
| 159 | Batching.messageOf(error), |
| 160 | ) |
| 161 | }), |
| 162 | ) |
| 163 | |
| 164 | return sending |
| 165 | } |
| 166 | |
| 167 | function queued(fields: Entries.Fields) { |
| 168 | priming ??= primed().catch(() => undefined) |
| 169 | |
| 170 | pending.push({ |
| 171 | fields, |
| 172 | eventId: crypto.randomUUID(), |
| 173 | loggedAt: new Date().toISOString(), |
| 174 | }) |
| 175 | |
| 176 | if (pending.length >= Batching.BATCH_ROWS) { |
| 177 | void flush() |
| 178 | |
| 179 | return |
| 180 | } |
| 181 | |
| 182 | timer ??= deps.after(Batching.BATCH_WINDOW_MS, () => void flush()) |
| 183 | } |
| 184 | |
| 185 | const telemetry: Telemetry = { |
| 186 | log: async entry => queued(Entries.checkedFields(entry)), |
| 187 | mark: async entry => queued(Entries.checkedMark(entry)), |
| 188 | } |
| 189 | |
| 190 | return { telemetry, flush } |
| 191 | } |