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 / telemetry.test.ts
189 lines · 4.5 KBRaw
1import type {
2 Args,
3 CommandRunInput,
4 HttpResponse,
5 On,
6 SessionAuthorization,
7} from 'claude-code'
8import type { Plugin } from 'claude-code/testing'
9import { expect, memoryEnv, seat, test } from 'claude-code/testing'
10
11import type { LogEntry } from '../hooks/telemetry-types'
12
13seat('builtin')
14
15const BEARER: SessionAuthorization = { handle: 'the-handle', kind: 'bearer' }
16const ACCEPTED: HttpResponse = { status: 200, ok: true, headers: {}, text: '' }
17
18const SURVEY: LogEntry = {
19 event: 'survey_answered',
20 props: { answer: 2, seen: true },
21}
22
23/**
24 * `/record <entry>`, which the recording plugin answers.
25 */
26const record = (entry: LogEntry): CommandRunInput => ({
27 command: 'record',
28 args: JSON.stringify(entry),
29 origin: { kind: 'composer' },
30})
31
32/**
33 * A plugin whose `/record <entry>` logs the entry through `$.telemetry`,
34 * answering "sent", or why the row was refused.
35 */
36const recording: Plugin = {
37 name: 'recording',
38 register(on) {
39 on('command.run', { command: 'record' }, ($, e) =>
40 $.telemetry.log(JSON.parse(e.args)).then(
41 () => ({ text: 'sent' }),
42 (error: unknown) => ({ text: String(error) }),
43 ),
44 )
45 },
46}
47
48/**
49 * A first-party session: its id and model, the credential it holds, and
50 * the ingest accepting each row, each post kept as it was made.
51 */
52function firstPartySession(
53 on: On,
54 authorization: SessionAuthorization = BEARER,
55) {
56 const posts: Args<'http.fetch'>[] = []
57 on('session.id', () => ({ value: 'the-session' }))
58 on('session.model', () => ({ value: 'the-model' }))
59 on('session.authorize', () => ({ value: authorization }))
60 on('http.fetch', ($, e) => {
61 posts.push(e)
62
63 return { value: ACCEPTED }
64 })
65
66 return posts
67}
68
69/**
70 * The batch a post to the ingest carries, as it was sent.
71 */
72function batchOf(post: Args<'http.fetch'>): unknown {
73 return JSON.parse(String(post.init?.body))
74}
75
76test(
77 'a $.telemetry.log call from a plugin posts one first-party row',
78 { plugins: [recording] },
79 async ($, on) => {
80 memoryEnv(on, { USER_TYPE: 'ant' })
81 const posts = firstPartySession(on)
82
83 expect(await $.command.run(record(SURVEY))).toEqual({
84 text: 'sent',
85 })
86 expect(posts).toHaveLength(1)
87
88 const [post] = posts
89
90 expect(post.init).toMatchObject({ method: 'POST', auth: 'the-handle' })
91 expect(batchOf(post)).toMatchObject({
92 events: [
93 {
94 event_data: {
95 event_name: 'tengu_plugin_survey_answered',
96 session_id: 'the-session',
97 model: 'the-model',
98 user_type: 'ant',
99 },
100 },
101 ],
102 })
103 },
104)
105
106test(
107 'a row already named tengu_ is sent under its own name',
108 { plugins: [recording] },
109 async ($, on) => {
110 memoryEnv(on, {})
111 const posts = firstPartySession(on)
112
113 await $.command.run(
114 record({ ...SURVEY, event: 'tengu_repl_diff_panel_shown' }),
115 )
116
117 expect(posts.map(batchOf)).toMatchObject([
118 {
119 events: [
120 {
121 event_data: {
122 event_name: 'tengu_repl_diff_panel_shown',
123 user_type: 'external',
124 },
125 },
126 ],
127 },
128 ])
129 },
130)
131
132test(
133 'nothing is sent for a person who asked not to be tracked',
134 { plugins: [recording] },
135 async ($, on) => {
136 memoryEnv(on, { DO_NOT_TRACK: '1' })
137 const posts = firstPartySession(on)
138
139 expect(await $.command.run(record(SURVEY))).toEqual({
140 text: 'sent',
141 })
142 expect(posts).toEqual([])
143 },
144)
145
146test(
147 'nothing is sent on a third-party provider',
148 { plugins: [recording] },
149 async ($, on) => {
150 memoryEnv(on, { CLAUDE_CODE_USE_BEDROCK: '1' })
151 const posts = firstPartySession(on)
152
153 await $.command.run(record(SURVEY))
154
155 expect(posts).toEqual([])
156 },
157)
158
159test(
160 'a session with no first-party credential is refused, nothing sent',
161 { plugins: [recording] },
162 async ($, on) => {
163 memoryEnv(on, {})
164 const posts = firstPartySession(on, null)
165 const { text } = await $.command.run(record(SURVEY))
166
167 expect(text).toEndWith(
168 '$.telemetry.log: this session has no first-party credential to ' +
169 'authorize',
170 )
171 expect(posts).toEqual([])
172 },
173)
174
175test(
176 'free text in a row is refused, nothing sent',
177 { plugins: [recording] },
178 async ($, on) => {
179 memoryEnv(on, {})
180 const posts = firstPartySession(on)
181 const { text } = await $.command.run({
182 ...record(SURVEY),
183 args: '{"event":"survey_answered","props":{"note":"hello world"}}',
184 })
185
186 expect(text).toContain('props.note: free text is refused')
187 expect(posts).toEqual([])
188 },
189)