spacehub

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

claude-code / mods / telemetry / hooks / telemetry-of / telemetry-of.ts
191 lines · 4.8 KBRaw
1import type { HttpResponse, Timer } from 'claude-code'
2
3import type { Telemetry } from '../../types'
4import Batching from '../batching'
5import Context from '../context'
6import Entries from '../entries'
7import type { Environment } from '../environment'
8import { isAnalyticsOff } from '../is-analytics-off'
9import type { Sender } from '../sender'
10import 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 */
23export 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}