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,15 +1,16 @@ | ||
| 1 | 1 | # telemetry |
| 2 | 2 | |
| 3 | -Plugin analytics as a plugin: one `engine.create` step adds `$.telemetry` | |
| 4 | -to the engine interface every plugin above it is handed, built over the | |
| 3 | +Plugin analytics as a plugin: one `engine.create` step adds `$.telemetry` to | |
| 4 | +the engine interface every plugin above it is handed, built over the | |
| 5 | 5 | `$.session` and `$.http` nouns beneath. `$.telemetry.log({ event, props })` |
| 6 | 6 | sends one event as one first-party row, `tengu_plugin_<event>`; |
| 7 | -`$.telemetry.mark({ feature, kind, reason? })` marks one use of a feature as | |
| 8 | -the CLI's own feature events do, `tengu_feature_<kind>` with a | |
| 9 | -`feature_name`. Each call is one POST to the event-logging ingest with the | |
| 10 | -session's own credential (`$.session.authorize()`, resolved at each call), | |
| 11 | -one attempt, nothing batched; a session with no first-party credential, or | |
| 12 | -an ingest that refuses, rejects the caller's promise. | |
| 7 | +`$.telemetry.mark({ feature, kind, reason?, props? })` marks one use of a | |
| 8 | +feature as the CLI's own feature events do, `tengu_feature_<kind>` with a | |
| 9 | +`feature_name` and the mark's properties beside it. Each call is one POST to | |
| 10 | +the event-logging ingest with the session's own credential | |
| 11 | +(`$.session.authorize()`, resolved at each call), one attempt, nothing | |
| 12 | +batched; a session with no first-party credential, or an ingest that | |
| 13 | +refuses, rejects the caller's promise. | |
| 13 | 14 | |
| 14 | 15 | It sends nothing wherever the CLI's own analytics are off: under |
| 15 | 16 | `DISABLE_TELEMETRY`, `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or |
| @@ -22,9 +23,10 @@ | ||
| 22 | 23 | |
| 23 | 24 | Nothing free-form reaches a row. An event name and every property key is a |
| 24 | 25 | snake_case token; a value is a finite number, a boolean, or a Choice (a |
| 25 | -string named together with the list it is chosen from); `mark` takes `ok`, | |
| 26 | -`sad` or `bad`, with a `reason` required on the last two and refused on the | |
| 27 | -first. An entry that breaks a rule is refused before anything is sent. | |
| 26 | +string named together with the list it is chosen from), under `log` and | |
| 27 | +`mark` alike; `mark` takes `ok`, `sad` or `bad`, with a `reason` required on | |
| 28 | +the last two and refused on the first. An entry that breaks a rule is | |
| 29 | +refused before anything is sent. | |
| 28 | 30 | |
| 29 | 31 | `hooks/register.ts` is the module; `hooks/telemetry-types/` is the noun's |
| 30 | 32 | type as a caller sees it. |
| @@ -1,8 +1,7 @@ | ||
| 1 | -import { checkedValue } from './checked-value' | |
| 1 | +import { checkedProps } from './checked-props' | |
| 2 | 2 | import type { Fields } from './fields' |
| 3 | 3 | import { fieldsOf } from './fields-of' |
| 4 | 4 | import { isRecord } from './is-record' |
| 5 | -import { PROP_LIMIT } from './prop-limit' | |
| 6 | 5 | import { refusal } from './refusal' |
| 7 | 6 | import { TOKEN } from './token' |
| 8 | 7 | |
| @@ -27,25 +26,5 @@ | ||
| 27 | 26 | throw refusal('takes an event name, a snake_case token') |
| 28 | 27 | } |
| 29 | 28 | |
| 30 | - if (!isRecord(props)) { | |
| 31 | - throw refusal('props: an object of properties by key') | |
| 32 | - } | |
| 33 | - | |
| 34 | - const entries = Object.entries(props) | |
| 35 | - | |
| 36 | - if (entries.length > PROP_LIMIT) { | |
| 37 | - throw refusal(`props: at most ${PROP_LIMIT} properties`) | |
| 38 | - } | |
| 39 | - | |
| 40 | - const checked: Record<string, string | number | boolean> = {} | |
| 41 | - | |
| 42 | - for (const [key, value] of entries) { | |
| 43 | - if (!TOKEN.test(key)) { | |
| 44 | - throw refusal('props: every key is a snake_case token') | |
| 45 | - } | |
| 46 | - | |
| 47 | - checked[key] = checkedValue(key, value) | |
| 48 | - } | |
| 49 | - | |
| 50 | - return fieldsOf(event, checked) | |
| 29 | + return fieldsOf(event, checkedProps(props, 'log')) | |
| 51 | 30 | } |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import { checkedProps } from './checked-props' | |
| 1 | 2 | import type { Fields } from './fields' |
| 2 | 3 | import { isMarkKind } from './is-mark-kind' |
| 3 | 4 | import { isRecord } from './is-record' |
| @@ -7,17 +8,19 @@ | ||
| 7 | 8 | |
| 8 | 9 | /** |
| 9 | 10 | * The row's fields from one mark, or a refusal naming the first thing |
| 10 | - * wrong: the entry's shape, the feature, the kind, then the reason. | |
| 11 | + * wrong: the entry's shape, the feature, the kind, the reason, then each | |
| 12 | + * property in turn. | |
| 11 | 13 | * |
| 12 | - * @param entry the feature, kind and reason as the caller passed them | |
| 14 | + * @param entry the feature, kind, reason and properties as the caller passed | |
| 15 | + * them | |
| 13 | 16 | * @returns the mark's row fields once the entry passes every check |
| 14 | 17 | */ |
| 15 | 18 | export function checkedMark(entry: unknown): Fields { |
| 16 | 19 | if (!isRecord(entry)) { |
| 17 | - throw refusal('takes one entry, { feature, kind, reason? }', 'mark') | |
| 20 | + throw refusal('takes one entry, { feature, kind, reason?, props? }', 'mark') | |
| 18 | 21 | } |
| 19 | 22 | |
| 20 | - const { feature, kind, reason } = entry | |
| 23 | + const { feature, kind, reason, props = {} } = entry | |
| 21 | 24 | |
| 22 | 25 | if (typeof feature !== 'string' || !TOKEN.test(feature)) { |
| 23 | 26 | throw refusal('takes a feature name, a snake_case token', 'mark') |
| @@ -32,7 +35,7 @@ | ||
| 32 | 35 | throw refusal('reason: an ok mark carries none', 'mark') |
| 33 | 36 | } |
| 34 | 37 | |
| 35 | - return markFieldsOf(kind, feature) | |
| 38 | + return markFieldsOf(kind, feature, undefined, checkedProps(props, 'mark')) | |
| 36 | 39 | } |
| 37 | 40 | |
| 38 | 41 | if (typeof reason !== 'string' || !TOKEN.test(reason)) { |
| @@ -42,5 +45,5 @@ | ||
| 42 | 45 | ) |
| 43 | 46 | } |
| 44 | 47 | |
| 45 | - return markFieldsOf(kind, feature, reason) | |
| 48 | + return markFieldsOf(kind, feature, reason, checkedProps(props, 'mark')) | |
| 46 | 49 | } |
| @@ -1,0 +1,41 @@ | ||
| 1 | +import type TelemetryTypes from '../../telemetry-types' | |
| 2 | +import { checkedValue } from '../checked-value' | |
| 3 | +import { isRecord } from '../is-record' | |
| 4 | +import { PROP_LIMIT } from '../prop-limit' | |
| 5 | +import { refusal } from '../refusal' | |
| 6 | +import { TOKEN } from '../token' | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * An entry's properties as they go into the row, or a refusal naming the | |
| 10 | + * first thing wrong: the shape, the count, then each key and value in turn. | |
| 11 | + * | |
| 12 | + * @param props what the caller passed as `props` | |
| 13 | + * @param method the method the properties were passed to, named in a refusal | |
| 14 | + * @returns the properties by key, each value checked | |
| 15 | + */ | |
| 16 | +export function checkedProps( | |
| 17 | + props: unknown, | |
| 18 | + method: TelemetryTypes.Method, | |
| 19 | +): Record<string, string | number | boolean> { | |
| 20 | + if (!isRecord(props)) { | |
| 21 | + throw refusal('props: an object of properties by key', method) | |
| 22 | + } | |
| 23 | + | |
| 24 | + const entries = Object.entries(props) | |
| 25 | + | |
| 26 | + if (entries.length > PROP_LIMIT) { | |
| 27 | + throw refusal(`props: at most ${PROP_LIMIT} properties`, method) | |
| 28 | + } | |
| 29 | + | |
| 30 | + const checked: Record<string, string | number | boolean> = {} | |
| 31 | + | |
| 32 | + for (const [key, value] of entries) { | |
| 33 | + if (!TOKEN.test(key)) { | |
| 34 | + throw refusal('props: every key is a snake_case token', method) | |
| 35 | + } | |
| 36 | + | |
| 37 | + checked[key] = checkedValue(key, value, method) | |
| 38 | + } | |
| 39 | + | |
| 40 | + return checked | |
| 41 | +} |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export * from './checked-props.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import type TelemetryTypes from '../../telemetry-types' | |
| 1 | 2 | import { CHOICE_TOKEN } from '../choice-token' |
| 2 | 3 | import { CHOICES_LIMIT } from '../choices-limit' |
| 3 | 4 | import { isRecord } from '../is-record' |
| @@ -11,12 +12,14 @@ | ||
| 11 | 12 | * |
| 12 | 13 | * @param key the property's key, named in a refusal |
| 13 | 14 | * @param value what the caller passed under it |
| 15 | + * @param method the method the property was passed to, named in a refusal | |
| 14 | 16 | * @returns the value as stored: the boolean or finite number unchanged, or the |
| 15 | 17 | * chosen member when value is a Choice |
| 16 | 18 | */ |
| 17 | 19 | export function checkedValue( |
| 18 | 20 | key: string, |
| 19 | 21 | value: unknown, |
| 22 | + method: TelemetryTypes.Method = 'log', | |
| 20 | 23 | ): string | number | boolean { |
| 21 | 24 | if (typeof value === 'boolean') { |
| 22 | 25 | return value |
| @@ -27,13 +30,14 @@ | ||
| 27 | 30 | return value |
| 28 | 31 | } |
| 29 | 32 | |
| 30 | - throw refusal(`props.${key}: a number is finite`) | |
| 33 | + throw refusal(`props.${key}: a number is finite`, method) | |
| 31 | 34 | } |
| 32 | 35 | |
| 33 | 36 | if (typeof value === 'string') { |
| 34 | 37 | throw refusal( |
| 35 | 38 | `props.${key}: free text is refused; a string is a Choice, ` + |
| 36 | 39 | `{ value, of: [...] }`, |
| 40 | + method, | |
| 37 | 41 | ) |
| 38 | 42 | } |
| 39 | 43 | |
| @@ -41,6 +45,7 @@ | ||
| 41 | 45 | throw refusal( |
| 42 | 46 | `props.${key}: a value is a finite number, a boolean, or a Choice, ` + |
| 43 | 47 | `{ value, of: [...] }`, |
| 48 | + method, | |
| 44 | 49 | ) |
| 45 | 50 | } |
| 46 | 51 | |
| @@ -56,11 +61,12 @@ | ||
| 56 | 61 | if (!isTokenList) { |
| 57 | 62 | throw refusal( |
| 58 | 63 | `props.${key}.of: a list of 1 to ${CHOICES_LIMIT} ` + `snake_case tokens`, |
| 64 | + method, | |
| 59 | 65 | ) |
| 60 | 66 | } |
| 61 | 67 | |
| 62 | 68 | if (typeof chosen !== 'string' || !members.includes(chosen)) { |
| 63 | - throw refusal(`props.${key}.value: one of the members of \`of\``) | |
| 69 | + throw refusal(`props.${key}.value: one of the members of \`of\``, method) | |
| 64 | 70 | } |
| 65 | 71 | |
| 66 | 72 | return chosen |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | export * from './batch-of.js' |
| 2 | 2 | export * from './checked-fields.js' |
| 3 | 3 | export * from './checked-mark.js' |
| 4 | +export * from './checked-props' | |
| 4 | 5 | export * from './checked-value' |
| 5 | 6 | export * from './choice-token' |
| 6 | 7 | export * from './choices-limit' |
| @@ -4,21 +4,26 @@ | ||
| 4 | 4 | |
| 5 | 5 | /** |
| 6 | 6 | * One checked mark as its fields: the CLI's own feature event, |
| 7 | - * `tengu_feature_<kind>` with `feature_name`, and `error_code` when given. | |
| 7 | + * `tengu_feature_<kind>` with `feature_name`, `error_code` when given, and | |
| 8 | + * the mark's properties merged in as the CLI's feature events merge their | |
| 9 | + * extras: after `feature_name` on an ok row, and beneath `feature_name` and | |
| 10 | + * `error_code` on a sad or bad one. | |
| 8 | 11 | * |
| 9 | 12 | * @param kind how the feature went |
| 10 | 13 | * @param feature the feature marked |
| 11 | 14 | * @param reason why, on a sad or bad mark; absent on ok |
| 15 | + * @param props the mark's checked properties | |
| 12 | 16 | * @returns the event's name and props, ready to log |
| 13 | 17 | */ |
| 14 | 18 | export const markFieldsOf = ( |
| 15 | 19 | kind: TelemetryTypes.MarkKind, |
| 16 | 20 | feature: string, |
| 17 | 21 | reason?: string, |
| 22 | + props: Readonly<Record<string, string | number | boolean>> = {}, | |
| 18 | 23 | ): Fields => ({ |
| 19 | 24 | name: FEATURE_PREFIX + kind, |
| 20 | 25 | props: |
| 21 | 26 | reason === undefined |
| 22 | - ? { feature_name: feature } | |
| 23 | - : { feature_name: feature, error_code: reason }, | |
| 27 | + ? { feature_name: feature, ...props } | |
| 28 | + : { ...props, feature_name: feature, error_code: reason }, | |
| 24 | 29 | }) |
| @@ -1,11 +1,13 @@ | ||
| 1 | 1 | import type { MarkKind } from '../mark-kind' |
| 2 | +import type { Prop } from '../prop' | |
| 2 | 3 | |
| 3 | 4 | /** |
| 4 | - * What `$.telemetry.mark` takes: the feature, how it went, and why when not | |
| 5 | - * ok. | |
| 5 | + * What `$.telemetry.mark` takes: the feature, how it went, why when not | |
| 6 | + * ok, and the properties the row carries beside them by snake_case key. | |
| 6 | 7 | */ |
| 7 | 8 | export type MarkEntry = { |
| 8 | 9 | feature: string |
| 9 | 10 | kind: MarkKind |
| 10 | 11 | reason?: string |
| 12 | + props?: Readonly<Record<string, Prop>> | |
| 11 | 13 | } |
| @@ -36,15 +36,22 @@ | ||
| 36 | 36 | * Marks one use of a feature as the CLI's own feature events do, one |
| 37 | 37 | * `tengu_feature_<kind>` row; resolves once the ingest accepted it. |
| 38 | 38 | * |
| 39 | - * The row carries `feature_name`, and `error_code` on a sad or bad one; it | |
| 40 | - * joins the product-wide feature surface (queried by `feature_name`), so | |
| 41 | - * no plugin prefix. `reason` is required on sad and bad, refused on ok. | |
| 39 | + * The row carries `feature_name`, `error_code` on a sad or bad one, and | |
| 40 | + * the entry's `props` beside them, checked as `log`'s are; it joins the | |
| 41 | + * product-wide feature surface (queried by `feature_name`), so no plugin | |
| 42 | + * prefix. `reason` is required on sad and bad, refused on ok. | |
| 42 | 43 | * |
| 43 | - * @param entry the feature, how it went, and why when not ok | |
| 44 | + * @param entry the feature, how it went, why when not ok, and the row's | |
| 45 | + * properties by snake_case key | |
| 44 | 46 | * @example |
| 45 | 47 | * await $.telemetry.mark({ feature: "learn_page", kind: "ok" }) |
| 46 | 48 | * await $.telemetry.mark({ |
| 47 | 49 | * feature: "learn_page", |
| 50 | + * kind: "ok", | |
| 51 | + * props: { page: { value: "later", of: ["ready", "later"] } }, | |
| 52 | + * }) | |
| 53 | + * await $.telemetry.mark({ | |
| 54 | + * feature: "learn_page", | |
| 48 | 55 | * kind: "sad", |
| 49 | 56 | * reason: "blocked", |
| 50 | 57 | * }) |