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 / tests / register.test.ts
369 lines · 9.8 KBRaw
1import type { Args } from 'claude-code'
2import { describe, expect, mock, test, tier } from 'claude-code/testing'
3
4import Fixtures from './fixtures'
5
6tier('builtin')
7
8describe('register', () => {
9 test(
10 'a $.telemetry.log call from a plugin posts one first-party row',
11 { plugins: [Fixtures.recording] },
12 async ($, on) => {
13 mock.env(on, { USER_TYPE: 'ant' })
14
15 const posts = Fixtures.firstPartySession(on)
16
17 expect(
18 await $.command.run(Fixtures.record(Fixtures.surveyAnswer())),
19 ).toEqual({ text: 'sent' })
20
21 expect(
22 posts.map(post => [post.url, post.init?.method, post.init?.auth]),
23 ).toEqual([
24 [
25 'https://api.anthropic.com/api/event_logging/v2/batch',
26 'POST',
27 'the-handle',
28 ],
29 ])
30
31 expect(posts.map(Fixtures.rowOf)).toEqual([
32 {
33 event_type: 'ClaudeCodeInternalEvent',
34 hasTimestamp: true,
35 event_name: 'tengu_plugin_survey_answered',
36 session_id: 'the-session',
37 model: 'the-model',
38 user_type: 'ant',
39 metadata: { answer: 2, page: 'ready', seen: true },
40 },
41 ])
42 },
43 )
44
45 test(
46 'a row already named tengu_ is sent under its own name',
47 { plugins: [Fixtures.recording] },
48 async ($, on) => {
49 mock.env(on, {})
50
51 const posts = Fixtures.firstPartySession(on)
52
53 await $.command.run(
54 Fixtures.record({
55 ...Fixtures.surveyAnswer(),
56 event: 'tengu_repl_diff_panel_shown',
57 }),
58 )
59
60 expect(posts.map(Fixtures.batchOf)).toMatchObject([
61 {
62 events: [
63 {
64 event_data: {
65 event_name: 'tengu_repl_diff_panel_shown',
66 user_type: 'external',
67 },
68 },
69 ],
70 },
71 ])
72 },
73 )
74
75 test(
76 'nothing is sent where any switch has analytics off',
77 { plugins: [Fixtures.recording] },
78 async ($, on) => {
79 let environment: Readonly<Record<string, string>> = {}
80
81 on('env.get', ($, e) => ({ value: environment[e.name] }))
82
83 const posts = Fixtures.firstPartySession(on)
84 const answers: (string | undefined)[] = []
85
86 for (const off of Fixtures.ANALYTICS_OFF_ENVIRONMENTS) {
87 environment = { USER_TYPE: 'ant', ...off }
88
89 answers.push(
90 (await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))).text,
91 )
92 }
93
94 const withheld = posts.length
95
96 environment = { USER_TYPE: 'ant' }
97
98 await $.command.run(Fixtures.record(Fixtures.surveyAnswer()))
99
100 expect(answers).toEqual(
101 Fixtures.ANALYTICS_OFF_ENVIRONMENTS.map(() => 'sent'),
102 )
103
104 expect(withheld, 'nothing posted while any switch was off').toBe(0)
105 expect(posts, 'then one row once every switch is clear').toHaveLength(1)
106 },
107 )
108
109 test(
110 'a third-party provider the host manages sends nothing too',
111 { plugins: [Fixtures.recording] },
112 async ($, on) => {
113 mock.env(on, {
114 CLAUDE_CODE_USE_BEDROCK: '1',
115 CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: '1',
116 })
117
118 const posts = Fixtures.firstPartySession(on)
119
120 const { text } = await $.command.run(
121 Fixtures.record(Fixtures.surveyAnswer()),
122 )
123
124 expect({ text, posts }).toEqual({ text: 'sent', posts: [] })
125 },
126 )
127
128 test(
129 'a $.telemetry.mark call posts the feature row by its own name',
130 { plugins: [Fixtures.marking] },
131 async ($, on) => {
132 mock.env(on, { USER_TYPE: 'ant' })
133
134 const posts = Fixtures.firstPartySession(on)
135 const answers: (string | undefined)[] = []
136
137 for (const entry of [
138 { feature: 'learn_page', kind: 'ok' },
139 { feature: 'learn_page', kind: 'sad', reason: 'blocked' },
140 { feature: 'suggest_learning', kind: 'bad', reason: 'api_error' },
141 ]) {
142 answers.push((await $.command.run(Fixtures.mark(entry))).text)
143 }
144
145 expect(answers).toEqual(['sent', 'sent', 'sent'])
146
147 expect(
148 posts.map(Fixtures.rowOf),
149 'the feature events by their own names, no plugin prefix',
150 ).toMatchObject([
151 {
152 event_name: 'tengu_feature_ok',
153 metadata: { feature_name: 'learn_page' },
154 },
155 {
156 event_name: 'tengu_feature_sad',
157 metadata: { feature_name: 'learn_page', error_code: 'blocked' },
158 },
159 {
160 event_name: 'tengu_feature_bad',
161 metadata: {
162 feature_name: 'suggest_learning',
163 error_code: 'api_error',
164 },
165 },
166 ])
167 },
168 )
169
170 test(
171 'a mark with a bad kind or a wrong reason is refused, nothing sent',
172 { plugins: [Fixtures.marking] },
173 async ($, on) => {
174 mock.env(on, { USER_TYPE: 'ant' })
175
176 const posts = Fixtures.firstPartySession(on)
177
178 const refusalFor = async (entry: unknown) =>
179 (await $.command.run(Fixtures.mark(entry))).text
180
181 expect(
182 await refusalFor({ feature: 'learn_page', kind: 'meh' }),
183 ).toEndWith("$.telemetry.mark: kind: 'ok', 'sad' or 'bad'")
184
185 expect(
186 await refusalFor({ feature: 'learn_page', kind: 'bad' }),
187 ).toEndWith(
188 '$.telemetry.mark: reason: a bad mark names why, a snake_case token',
189 )
190
191 expect(
192 await refusalFor({ feature: 'learn_page', kind: 'ok', reason: 'x' }),
193 ).toEndWith('$.telemetry.mark: reason: an ok mark carries none')
194
195 expect(await refusalFor({ feature: 'Learn Page', kind: 'ok' })).toEndWith(
196 '$.telemetry.mark: takes a feature name, a snake_case token',
197 )
198
199 expect(await refusalFor('x')).toEndWith(
200 '$.telemetry.mark: takes one entry, { feature, kind, reason?, props? }',
201 )
202
203 expect(posts).toEqual([])
204 },
205 )
206
207 test(
208 'a session with no first-party credential is refused, nothing sent',
209 { plugins: [Fixtures.recording] },
210 async ($, on) => {
211 mock.env(on, {})
212
213 const posts = Fixtures.firstPartySession(on, null)
214
215 const { text } = await $.command.run(
216 Fixtures.record(Fixtures.surveyAnswer()),
217 )
218
219 expect(text).toEndWith(
220 '$.telemetry.log: this session has no first-party credential to ' +
221 'authorize',
222 )
223
224 expect(posts).toEqual([])
225 },
226 )
227
228 test(
229 "the ingest's refusal rejects the caller's promise",
230 { plugins: [Fixtures.recording] },
231 async ($, on) => {
232 mock.env(on, {})
233
234 const posts = Fixtures.firstPartySession(
235 on,
236 Fixtures.BEARER,
237 Fixtures.REFUSED,
238 )
239
240 const { text } = await $.command.run(
241 Fixtures.record(Fixtures.surveyAnswer()),
242 )
243
244 expect(text).toEndWith('$.telemetry.log: the ingest answered 500')
245 expect(posts).toHaveLength(1)
246 },
247 )
248
249 test(
250 'free text and a malformed entry are refused, nothing sent',
251 { plugins: [Fixtures.recording] },
252 async ($, on) => {
253 mock.env(on, {})
254
255 const posts = Fixtures.firstPartySession(on)
256
257 const refusalFor = async (entry: unknown) =>
258 (
259 await $.command.run({
260 ...Fixtures.record(Fixtures.surveyAnswer()),
261 args: JSON.stringify(entry),
262 })
263 ).text
264
265 expect(
266 await refusalFor({ event: 'x', props: { note: 'hello world' } }),
267 ).toEndWith(
268 '$.telemetry.log: props.note: free text is refused; a string is a ' +
269 'Choice, { value, of: [...] }',
270 )
271
272 expect(
273 await refusalFor({
274 event: 'x',
275 props: { page: { value: 'elsewhere', of: ['ready', 'later'] } },
276 }),
277 ).toEndWith(
278 '$.telemetry.log: props.page.value: one of the members of `of`',
279 )
280
281 expect(await refusalFor({ event: 'Survey' })).toEndWith(
282 '$.telemetry.log: takes an event name, a snake_case token',
283 )
284
285 expect(
286 await refusalFor({ event: 'x', props: { 'a path': 1 } }),
287 ).toEndWith('$.telemetry.log: props: every key is a snake_case token')
288
289 expect(await refusalFor('x')).toEndWith(
290 '$.telemetry.log: takes one entry, { event, props? }',
291 )
292
293 expect(posts).toEqual([])
294 },
295 )
296
297 test(
298 'a number that is not finite is refused, nothing sent',
299 {
300 plugins: [
301 {
302 name: 'counting',
303 register(on) {
304 on('command.run', { command: 'count' }, $ =>
305 $.telemetry.log({ event: 'x', props: { n: Number.NaN } }).then(
306 () => ({ text: 'sent' }),
307 (error: unknown) => ({ text: String(error) }),
308 ),
309 )
310 },
311 },
312 ],
313 },
314 async ($, on) => {
315 mock.env(on, {})
316
317 const posts = Fixtures.firstPartySession(on)
318
319 const { text } = await $.command.run({
320 command: 'count',
321 args: '',
322 origin: { kind: 'composer' },
323 presentation: Fixtures.FULLSCREEN,
324 })
325
326 expect(text).toEndWith('$.telemetry.log: props.n: a number is finite')
327 expect(posts).toEqual([])
328 },
329 )
330
331 test(
332 'a failed authorize is not memoized; a retry sends',
333 { plugins: [Fixtures.recording] },
334 async ($, on) => {
335 const posts: Args<'http.fetch'>[] = []
336
337 let authorizations = 0
338
339 mock.env(on, {})
340 on('session.id', () => ({ value: 'the-session' }))
341 on('session.model', () => ({ value: 'the-model' }))
342
343 on('session.authorize', () => {
344 authorizations += 1
345
346 const isFirstAuthorization = authorizations === 1
347
348 return isFirstAuthorization
349 ? { deny: 'transient' }
350 : { value: Fixtures.BEARER }
351 })
352
353 on('http.fetch', ($, e) => {
354 posts.push(e)
355
356 return { value: Fixtures.ACCEPTED }
357 })
358
359 const first = await $.command.run(Fixtures.record({ event: 'x' }))
360 const postsAfterFirst = posts.length
361 const second = await $.command.run(Fixtures.record({ event: 'x' }))
362
363 expect(first.text).toContain('transient')
364 expect(postsAfterFirst).toBe(0)
365 expect(second.text).toBe('sent')
366 expect(posts).toHaveLength(1)
367 },
368 )
369})