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

mods: a noun's types live in its own types folder and dependents read them from thereBrowse files
poteat authored on Sep 12, 2026 parent 748e0c4 · commit b95b0150f8fb
33 changed files+208 -252compared with 748e0c4
Mmods/README.md
Dmods/diff/hooks/telemetry-noun.d.ts
Mmods/telemetry/.claude-plugin/plugin.json
Mmods/telemetry/README.md
Mmods/telemetry/hooks/entries/checked-props/checked-props.ts
Mmods/telemetry/hooks/entries/checked-value/checked-value.ts
Mmods/telemetry/hooks/entries/index.ts
Mmods/telemetry/hooks/entries/is-mark-kind/is-mark-kind.ts
Mmods/telemetry/hooks/entries/mark/mark.ts
Mmods/telemetry/hooks/entries/mark-kinds/mark-kinds.ts
Amods/telemetry/hooks/entries/method/index.ts
Amods/telemetry/hooks/entries/method/method.ts
Mmods/telemetry/hooks/entries/refusal/refusal.ts
Mmods/telemetry/hooks/index.ts
Mmods/telemetry/hooks/register.ts
Mmods/telemetry/hooks/telemetry-of/telemetry-of.ts
Dmods/telemetry/hooks/telemetry-types/choice/choice.ts
Dmods/telemetry/hooks/telemetry-types/choice/index.ts
Dmods/telemetry/hooks/telemetry-types/index.ts
Dmods/telemetry/hooks/telemetry-types/log-entry/index.ts
Dmods/telemetry/hooks/telemetry-types/log-entry/log-entry.ts
Dmods/telemetry/hooks/telemetry-types/mark-entry/index.ts
Dmods/telemetry/hooks/telemetry-types/mark-entry/mark-entry.ts
Dmods/telemetry/hooks/telemetry-types/mark-kind/index.ts
Dmods/telemetry/hooks/telemetry-types/mark-kind/mark-kind.ts
Dmods/telemetry/hooks/telemetry-types/method.ts
Dmods/telemetry/hooks/telemetry-types/prop/index.ts
Dmods/telemetry/hooks/telemetry-types/prop/prop.ts
Dmods/telemetry/hooks/telemetry-types/telemetry.ts
Mmods/telemetry/tests/fixtures/record.ts
Mmods/telemetry/tests/fixtures/survey-answer.ts
Amods/telemetry/types/index.d.ts
Mmods/tsconfig.json
@@ -76,8 +76,37 @@
7676 rendered, as a click in the terminal does.
7777
7878 `tsc -p mods/tsconfig.json` typechecks every mod's hooks and tests against
79-`types/`.
79+`types/` and each mod's own `types/` contract.
8080
81+## Composing mods: noun contracts
82+
83+A mod that adds a noun to `$` in the `engine.create` fold owns that noun's
84+types, and keeps them in one place: its `types/index.d.ts`, an ambient file
85+with no imports that merges into `claude-code`, declaring the noun on
86+`EngineInterface` and exporting the types it is made of, each named for the
87+noun (`telemetry/types/index.d.ts` declares `$.telemetry` and exports
88+`Telemetry`, `TelemetryLogEntry`, `TelemetryMarkEntry` and the rest).
89+
90+- The contract is the only declaration of the noun. The mod's own hooks
91+ import its types from `claude-code` (`import type { Telemetry } from
92+ 'claude-code'`), and the value its `engine.create` hook returns is checked
93+ against `EngineInterface['telemetry']`, so the implementation cannot drift
94+ from what callers read.
95+- A mod that calls another's noun reads the same file and never copies it:
96+ `mods/tsconfig.json` includes `*/types/**/*.d.ts`, so `$.telemetry.log(…)`
97+ in `diff` types against `telemetry`'s contract as it stands.
98+- A test of a mod that calls another's noun seats a provider for it, an inline
99+ plugin whose `engine.create` hook adds the noun, and answers the calls the
100+ way it answers the engine's: `on('telemetry.log', ($, e) => ({ value:
101+ undefined }))` runs above the provider's own method, its `e` typed by the
102+ contract. With no provider loaded the `$` build refuses the hook, naming the
103+ noun nobody provides.
104+
105+A plugin outside this repository that depends on a mod's noun points its
106+tsconfig `include` at that mod's `types/` folder for now; once the engine
107+writes the contracts of the plugins a session has installed, `/plugin-types`
108+will put them beside `claude-code.d.ts` and the include goes away.
109+
81110 Early access: hooks modules load only where function hooks are enabled, and
82111 the API these mods are written against may change between releases without
83112 notice. They are not listed in this repository's marketplace; the copies that
Dmods/diff/hooks/telemetry-noun.d.ts+0 -57
@@ -1,57 +1,0 @@
1-/**
2- * The `$.telemetry` noun as this plugin calls it, declared for a build of
3- * this folder on its own; no module imports it.
4- *
5- * The telemetry plugin adds the noun in the engine.create fold on internal
6- * builds and nowhere else: where no plugin provides it the calls throw and
7- * record/ drops the row. The engine's repository leaves this file out.
8- *
9- * @entry
10- */
11-declare module 'claude-code' {
12- /**
13- * How one use of a feature went: as hoped, degraded, or failed outright.
14- */
15- type DiffTelemetryMarkKind = 'ok' | 'sad' | 'bad'
16-
17- /**
18- * A string property: the value and the list it is chosen from.
19- */
20- type DiffTelemetryChoice = { value: string; of: readonly string[] }
21-
22- /**
23- * One property value a row may carry.
24- */
25- type DiffTelemetryProp = number | boolean | DiffTelemetryChoice
26-
27- /**
28- * What `$.telemetry.log` takes: the event's name after the prefix, and
29- * its properties by snake_case key.
30- */
31- type DiffTelemetryLogEntry = {
32- event: string
33- props?: Readonly<Record<string, DiffTelemetryProp>>
34- }
35-
36- /**
37- * What `$.telemetry.mark` takes: the feature, how it went, why when not
38- * ok, and the properties the row carries beside them by snake_case key.
39- */
40- type DiffTelemetryMarkEntry = {
41- feature: string
42- kind: DiffTelemetryMarkKind
43- reason?: string
44- props?: Readonly<Record<string, DiffTelemetryProp>>
45- }
46-
47- interface EngineInterface {
48- /**
49- * A plugin's analytics, one first-party row per call; present only
50- * where the telemetry plugin is seated.
51- */
52- telemetry: {
53- log: (entry: DiffTelemetryLogEntry) => Promise<void>
54- mark: (entry: DiffTelemetryMarkEntry) => Promise<void>
55- }
56- }
57-}
@@ -4,5 +4,6 @@
44 "description": "Plugin analytics: adds $.telemetry in the engine.create fold, so a plugin logs an event or marks a feature's use as one first-party row per call, sent with the session's own credential.",
55 "author": {
66 "name": "Anthropic"
7- }
7+ },
8+ "types": "types/index.d.ts"
89 }
@@ -28,8 +28,9 @@
2828 the last two and refused on the first. An entry that breaks a rule is
2929 refused before anything is sent.
3030
31-`hooks/register.ts` is the module; `hooks/telemetry-types/` is the noun's
32-type as a caller sees it.
31+`hooks/register.ts` is the module; `types/index.d.ts` is the noun's contract,
32+the one declaration of `$.telemetry` that this mod's hooks, a mod calling the
33+noun and a test answering it all read.
3334
3435 ## What it hooks
3536
@@ -1,6 +1,6 @@
1-import type TelemetryTypes from '../../telemetry-types'
21 import { checkedValue } from '../checked-value'
32 import { isRecord } from '../is-record'
3+import type { Method } from '../method'
44 import { PROP_LIMIT } from '../prop-limit'
55 import { refusal } from '../refusal'
66 import { TOKEN } from '../token'
@@ -15,7 +15,7 @@
1515 */
1616 export function checkedProps(
1717 props: unknown,
18- method: TelemetryTypes.Method,
18+ method: Method,
1919 ): Record<string, string | number | boolean> {
2020 if (!isRecord(props)) {
2121 throw refusal('props: an object of properties by key', method)
@@ -1,7 +1,7 @@
1-import type TelemetryTypes from '../../telemetry-types'
21 import { CHOICE_TOKEN } from '../choice-token'
32 import { CHOICES_LIMIT } from '../choices-limit'
43 import { isRecord } from '../is-record'
4+import type { Method } from '../method'
55 import { refusal } from '../refusal'
66
77 /**
@@ -19,7 +19,7 @@
1919 export function checkedValue(
2020 key: string,
2121 value: unknown,
22- method: TelemetryTypes.Method = 'log',
22+ method: Method = 'log',
2323 ): string | number | boolean {
2424 if (typeof value === 'boolean') {
2525 return value
@@ -16,6 +16,7 @@
1616 export * from './mark'
1717 export * from './mark-fields-of'
1818 export * from './mark-kinds'
19+export * from './method'
1920 export * from './prop-limit'
2021 export * from './refusal'
2122 export * from './row-session'
@@ -1,4 +1,5 @@
1-import type TelemetryTypes from '../../telemetry-types'
1+import type { TelemetryMarkKind } from 'claude-code'
2+
23 import { MARK_KINDS } from '../mark-kinds'
34
45 /**
@@ -7,5 +8,5 @@
78 * @param value what the caller passed as `kind`
89 * @returns whether value names one of the three mark kinds
910 */
10-export const isMarkKind = (value: unknown): value is TelemetryTypes.MarkKind =>
11+export const isMarkKind = (value: unknown): value is TelemetryMarkKind =>
1112 MARK_KINDS.some(kind => kind === value)
@@ -1,4 +1,5 @@
1-import type TelemetryTypes from '../../telemetry-types'
1+import type { TelemetryMarkKind } from 'claude-code'
2+
23 import type { Fields } from '../fields'
34
45 /**
@@ -6,7 +7,7 @@
67 * and its properties as they go into the row.
78 */
89 export type Mark = {
9- kind: TelemetryTypes.MarkKind
10+ kind: TelemetryMarkKind
1011 feature: string
1112 reason?: string
1213 props: Fields['props']
@@ -1,10 +1,6 @@
1-import type TelemetryTypes from '../../telemetry-types'
1+import type { TelemetryMarkKind } from 'claude-code'
22
33 /**
44 * The three kinds a mark may be, in the order the feature events name them.
55 */
6-export const MARK_KINDS: readonly TelemetryTypes.MarkKind[] = [
7- 'ok',
8- 'sad',
9- 'bad',
10-]
6+export const MARK_KINDS: readonly TelemetryMarkKind[] = ['ok', 'sad', 'bad']
@@ -1,0 +1,3 @@
1+export type * from './method.js'
2+
3+export * as default from '.'
@@ -1,0 +1,4 @@
1+/**
2+ * The two methods of `$.telemetry`, named in a refusal.
3+ */
4+export type Method = 'log' | 'mark'
@@ -1,4 +1,4 @@
1-import type TelemetryTypes from '../../telemetry-types'
1+import type { Method } from '../method'
22
33 /**
44 * The error a refused entry rejects with, naming the method and what was wrong.
@@ -11,7 +11,5 @@
1111 * @returns the error to reject the call with, naming the method and what was
1212 * wrong
1313 */
14-export const refusal = (
15- what: string,
16- method: TelemetryTypes.Method = 'log',
17-): Error => new Error(`$.telemetry.${method}: ${what}`)
14+export const refusal = (what: string, method: Method = 'log'): Error =>
15+ new Error(`$.telemetry.${method}: ${what}`)
@@ -4,6 +4,5 @@
44 export * from './register.js'
55 export * from './telemetry-deps'
66 export * from './telemetry-of'
7-export * from './telemetry-types'
87
98 export * as default from '.'
@@ -1,4 +1,4 @@
1-import type { On } from 'claude-code'
1+import type { EngineInterface, On } from 'claude-code'
22
33 import { telemetryOf } from './telemetry-of'
44
@@ -15,33 +15,30 @@
1515 on('engine.create', async ($, e, next) => {
1616 const beneath = await next(e)
1717
18- return {
19- ...beneath,
20- telemetry: telemetryOf({
21- authorize: () => beneath.session.authorize(),
22- id: () => beneath.session.id(),
23- model: () => beneath.session.model(),
24- environment: async () => ({
25- userType: await beneath.env.get('USER_TYPE'),
26- disableTelemetry: await beneath.env.get('DISABLE_TELEMETRY'),
27- disableNonessentialTraffic: await beneath.env.get(
28- 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
29- ),
30- doNotTrack: await beneath.env.get('DO_NOT_TRACK'),
31- customOauthUrl: await beneath.env.get('CLAUDE_CODE_CUSTOM_OAUTH_URL'),
32- useBedrock: await beneath.env.get('CLAUDE_CODE_USE_BEDROCK'),
33- useVertex: await beneath.env.get('CLAUDE_CODE_USE_VERTEX'),
34- useFoundry: await beneath.env.get('CLAUDE_CODE_USE_FOUNDRY'),
35- useAnthropicAws: await beneath.env.get(
36- 'CLAUDE_CODE_USE_ANTHROPIC_AWS',
37- ),
38- useAnthropicGoogleCloud: await beneath.env.get(
39- 'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
40- ),
41- useMantle: await beneath.env.get('CLAUDE_CODE_USE_MANTLE'),
42- }),
43- fetch: (url, init) => beneath.http.fetch(url, init),
18+ const telemetry: EngineInterface['telemetry'] = telemetryOf({
19+ authorize: () => beneath.session.authorize(),
20+ id: () => beneath.session.id(),
21+ model: () => beneath.session.model(),
22+ environment: async () => ({
23+ userType: await beneath.env.get('USER_TYPE'),
24+ disableTelemetry: await beneath.env.get('DISABLE_TELEMETRY'),
25+ disableNonessentialTraffic: await beneath.env.get(
26+ 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
27+ ),
28+ doNotTrack: await beneath.env.get('DO_NOT_TRACK'),
29+ customOauthUrl: await beneath.env.get('CLAUDE_CODE_CUSTOM_OAUTH_URL'),
30+ useBedrock: await beneath.env.get('CLAUDE_CODE_USE_BEDROCK'),
31+ useVertex: await beneath.env.get('CLAUDE_CODE_USE_VERTEX'),
32+ useFoundry: await beneath.env.get('CLAUDE_CODE_USE_FOUNDRY'),
33+ useAnthropicAws: await beneath.env.get('CLAUDE_CODE_USE_ANTHROPIC_AWS'),
34+ useAnthropicGoogleCloud: await beneath.env.get(
35+ 'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
36+ ),
37+ useMantle: await beneath.env.get('CLAUDE_CODE_USE_MANTLE'),
4438 }),
45- }
39+ fetch: (url, init) => beneath.http.fetch(url, init),
40+ })
41+
42+ return { ...beneath, telemetry }
4643 })
4744 }
@@ -1,7 +1,8 @@
1+import type { Telemetry } from 'claude-code'
2+
13 import Entries from '../entries'
24 import { isAnalyticsOff } from '../is-analytics-off'
35 import type { TelemetryDeps } from '../telemetry-deps'
4-import type TelemetryTypes from '../telemetry-types'
56
67 /**
78 * Builds `$.telemetry`: `log` and `mark` check the entry, read the environment
@@ -14,12 +15,12 @@
1415 * @param deps the calls on the nouns beneath
1516 * @returns the `$.telemetry` interface, `log` and `mark`
1617 */
17-export function telemetryOf(deps: TelemetryDeps): TelemetryTypes.Telemetry {
18+export function telemetryOf(deps: TelemetryDeps): Telemetry {
1819 let queue: Promise<unknown> = Promise.resolve()
1920
2021 async function post(
2122 fields: Entries.Fields,
22- method: TelemetryTypes.Method,
23+ method: Entries.Method,
2324 ): Promise<void> {
2425 const environment = await deps.environment()
2526
@@ -59,7 +60,7 @@
5960
6061 function queued(
6162 fields: Entries.Fields,
62- method: TelemetryTypes.Method,
63+ method: Entries.Method,
6364 ): Promise<void> {
6465 const turn = queue.then(() => post(fields, method))
6566 queue = turn.catch(() => undefined)
Dmods/telemetry/hooks/telemetry-types/choice/choice.ts+0 -9
@@ -1,9 +1,0 @@
1-/**
2- * A string property: the value and the list it is chosen from, declared
3- * beside it, so no free text reaches the row.
4- *
5- * Every member of `of` is a lowercase token of letters, digits, `_` and
6- * `-`, which may start with a digit, at most CHOICES_LIMIT of them; `value`
7- * is one of them.
8- */
9-export type Choice = { value: string; of: readonly string[] }
Dmods/telemetry/hooks/telemetry-types/choice/index.ts+0 -3
@@ -1,3 +1,0 @@
1-export type * from './choice.js'
2-
3-export * as default from '.'
Dmods/telemetry/hooks/telemetry-types/index.ts+0 -9
@@ -1,9 +1,0 @@
1-export * from './choice'
2-export * from './log-entry'
3-export * from './mark-entry'
4-export * from './mark-kind'
5-export type * from './method.js'
6-export * from './prop'
7-export type * from './telemetry.js'
8-
9-export * as default from '.'
Dmods/telemetry/hooks/telemetry-types/log-entry/index.ts+0 -3
@@ -1,3 +1,0 @@
1-export type * from './log-entry.js'
2-
3-export * as default from '.'
Dmods/telemetry/hooks/telemetry-types/log-entry/log-entry.ts+0 -10
@@ -1,10 +1,0 @@
1-import type { Prop } from '../prop'
2-
3-/**
4- * What `$.telemetry.log` takes: the event's name after the prefix, and its
5- * properties by snake_case key.
6- */
7-export type LogEntry = {
8- event: string
9- props?: Readonly<Record<string, Prop>>
10-}
Dmods/telemetry/hooks/telemetry-types/mark-entry/index.ts+0 -3
@@ -1,3 +1,0 @@
1-export type * from './mark-entry.js'
2-
3-export * as default from '.'
Dmods/telemetry/hooks/telemetry-types/mark-entry/mark-entry.ts+0 -13
@@ -1,13 +1,0 @@
1-import type { MarkKind } from '../mark-kind'
2-import type { Prop } from '../prop'
3-
4-/**
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.
7- */
8-export type MarkEntry = {
9- feature: string
10- kind: MarkKind
11- reason?: string
12- props?: Readonly<Record<string, Prop>>
13-}
Dmods/telemetry/hooks/telemetry-types/mark-kind/index.ts+0 -3
@@ -1,3 +1,0 @@
1-export type * from './mark-kind.js'
2-
3-export * as default from '.'
Dmods/telemetry/hooks/telemetry-types/mark-kind/mark-kind.ts+0 -8
@@ -1,8 +1,0 @@
1-/**
2- * How a feature went, as the CLI's own feature events count it.
3- *
4- * `ok`: used, the person got what they asked. `sad`: degraded, a fallback
5- * or a partial, the person still got something. `bad`: failed, the person
6- * got nothing.
7- */
8-export type MarkKind = 'ok' | 'sad' | 'bad'
Dmods/telemetry/hooks/telemetry-types/method.ts+0 -4
@@ -1,4 +1,0 @@
1-/**
2- * The two methods of `$.telemetry`, named in a refusal.
3- */
4-export type Method = 'log' | 'mark'
Dmods/telemetry/hooks/telemetry-types/prop/index.ts+0 -3
@@ -1,3 +1,0 @@
1-export type * from './prop.js'
2-
3-export * as default from '.'
Dmods/telemetry/hooks/telemetry-types/prop/prop.ts+0 -7
@@ -1,7 +1,0 @@
1-import type { Choice } from '../choice'
2-
3-/**
4- * A property's value: a finite number, a boolean, or a Choice; never free
5- * text.
6- */
7-export type Prop = number | boolean | Choice
Dmods/telemetry/hooks/telemetry-types/telemetry.ts+0 -57
@@ -1,57 +1,0 @@
1-import type { LogEntry } from './log-entry'
2-import type { MarkEntry } from './mark-entry'
3-
4-/**
5- * A plugin's analytics, sent one event at a time through `$.telemetry`.
6- *
7- * Internal builds alone: the telemetry built-in adds the noun in the
8- * engine.create fold, so a plugin on an external build, or one where the
9- * built-in is off, finds no `$.telemetry`.
10- */
11-export type Telemetry = {
12- /**
13- * Sends one event, `tengu_plugin_<event>`, as one first-party row;
14- * resolves once the ingest accepted it.
15- *
16- * The calling built-in names itself in `event`; one already named `tengu_…`
17- * is sent as named. A value is a finite number, a boolean or a Choice; free
18- * text is refused. One input, as every op on `$` takes.
19- *
20- * @param entry the event's name, a snake_case token, and its properties by
21- * snake_case key
22- * @example
23- * await $.telemetry.log({
24- * event: "suggest_learning_survey_answered",
25- * props: {
26- * answer: 2,
27- * page: { value: "ready", of: ["ready", "later"] },
28- * },
29- * })
30- */
31- log: (entry: LogEntry) => Promise<void>
32-
33- /**
34- * Marks one use of a feature as the CLI's own feature events do, one
35- * `tengu_feature_<kind>` row; resolves once the ingest accepted it.
36- *
37- * The row carries `feature_name`, `error_code` on sad or bad (`reason`,
38- * required there and refused on ok) and the entry's `props`, checked as
39- * `log`'s are; it joins the product-wide feature surface, so no prefix.
40- *
41- * @param entry the feature, how it went, why when not ok, and the row's
42- * properties by snake_case key
43- * @example
44- * await $.telemetry.mark({ feature: "learn_page", kind: "ok" })
45- * await $.telemetry.mark({
46- * feature: "learn_page",
47- * kind: "ok",
48- * props: { page: { value: "later", of: ["ready", "later"] } },
49- * })
50- * await $.telemetry.mark({
51- * feature: "learn_page",
52- * kind: "sad",
53- * reason: "blocked",
54- * })
55- */
56- mark: (entry: MarkEntry) => Promise<void>
57-}
@@ -1,7 +1,5 @@
1-import type { CommandRunInput } from 'claude-code'
1+import type { CommandRunInput, TelemetryLogEntry } from 'claude-code'
22
3-import type { LogEntry } from '../../hooks/telemetry-types'
4-
53 /**
64 * The command that has the recording plugin log an entry, typed as the
75 * person would type it.
@@ -9,7 +7,7 @@
97 * @param entry what to log
108 * @returns `/record <entry>`
119 */
12-export const record = (entry: LogEntry): CommandRunInput => ({
10+export const record = (entry: TelemetryLogEntry): CommandRunInput => ({
1311 command: 'record',
1412 args: JSON.stringify(entry),
1513 origin: { kind: 'composer' },
@@ -1,11 +1,11 @@
1-import type { LogEntry } from '../../hooks/telemetry-types'
1+import type { TelemetryLogEntry } from 'claude-code'
22
33 /**
44 * A survey answered, as a plugin logs it.
55 *
66 * @returns the entry, fresh each call
77 */
8-export const surveyAnswer = (): LogEntry => ({
8+export const surveyAnswer = (): TelemetryLogEntry => ({
99 event: 'survey_answered',
1010 props: { answer: 2, seen: true },
1111 })
@@ -1,0 +1,115 @@
1+/**
2+ * The `$.telemetry` noun as every caller sees it: the one contract for the
3+ * noun, merged into `claude-code` beside the engine's own declarations.
4+ *
5+ * The telemetry mod adds the noun in the `engine.create` fold and checks its
6+ * return against `EngineInterface['telemetry']`; a mod that calls it, and a
7+ * test that answers it with `on('telemetry.log', …)`, read the same types by
8+ * including this folder in their tsconfig. Nothing here is imported: the file
9+ * is ambient, so it merges wherever it is included.
10+ */
11+declare module 'claude-code' {
12+ interface EngineInterface {
13+ /**
14+ * A plugin's analytics, one first-party row per call; present only where
15+ * the telemetry mod is seated (internal builds), absent everywhere else.
16+ */
17+ telemetry: Telemetry
18+ }
19+
20+ /**
21+ * A plugin's analytics, sent one event at a time through `$.telemetry`.
22+ *
23+ * Internal builds alone: the telemetry mod adds the noun in the
24+ * `engine.create` fold, so a plugin on an external build, or one where the
25+ * mod is off, finds no `$.telemetry` and its call throws.
26+ */
27+ export type Telemetry = {
28+ /**
29+ * Sends one event, `tengu_plugin_<event>`, as one first-party row;
30+ * resolves once the ingest accepted it.
31+ *
32+ * The calling mod names itself in `event`; one already named `tengu_…` is
33+ * sent as named. A value is a finite number, a boolean or a
34+ * TelemetryChoice; free text is refused. One input, as every op on `$`
35+ * takes.
36+ *
37+ * @param entry the event's name, a snake_case token, and its properties by
38+ * snake_case key
39+ * @example
40+ * await $.telemetry.log({
41+ * event: "suggest_learning_survey_answered",
42+ * props: {
43+ * answer: 2,
44+ * page: { value: "ready", of: ["ready", "later"] },
45+ * },
46+ * })
47+ */
48+ log: (entry: TelemetryLogEntry) => Promise<void>
49+
50+ /**
51+ * Marks one use of a feature as the CLI's own feature events do, one
52+ * `tengu_feature_<kind>` row; resolves once the ingest accepted it.
53+ *
54+ * The row carries `feature_name`, `error_code` on sad or bad (`reason`,
55+ * required there and refused on ok) and the entry's `props`, checked as
56+ * `log`'s are; it joins the product-wide feature surface, so no prefix.
57+ *
58+ * @param entry the feature, how it went, why when not ok, and the row's
59+ * properties by snake_case key
60+ * @example
61+ * await $.telemetry.mark({ feature: "learn_page", kind: "ok" })
62+ * await $.telemetry.mark({
63+ * feature: "learn_page",
64+ * kind: "sad",
65+ * reason: "blocked",
66+ * })
67+ */
68+ mark: (entry: TelemetryMarkEntry) => Promise<void>
69+ }
70+
71+ /**
72+ * What `$.telemetry.log` takes: the event's name after the prefix, and its
73+ * properties by snake_case key.
74+ */
75+ export type TelemetryLogEntry = {
76+ event: string
77+ props?: Readonly<Record<string, TelemetryProp>>
78+ }
79+
80+ /**
81+ * What `$.telemetry.mark` takes: the feature, how it went, why when not
82+ * ok, and the properties the row carries beside them by snake_case key.
83+ */
84+ export type TelemetryMarkEntry = {
85+ feature: string
86+ kind: TelemetryMarkKind
87+ reason?: string
88+ props?: Readonly<Record<string, TelemetryProp>>
89+ }
90+
91+ /**
92+ * How a feature went, as the CLI's own feature events count it.
93+ *
94+ * `ok`: used, the person got what they asked. `sad`: degraded, a fallback
95+ * or a partial, the person still got something. `bad`: failed, the person
96+ * got nothing.
97+ */
98+ export type TelemetryMarkKind = 'ok' | 'sad' | 'bad'
99+
100+ /**
101+ * A property's value: a finite number, a boolean, or a TelemetryChoice;
102+ * never free text.
103+ */
104+ export type TelemetryProp = number | boolean | TelemetryChoice
105+
106+ /**
107+ * A string property: the value and the list it is chosen from, declared
108+ * beside it, so no free text reaches the row.
109+ *
110+ * Every member of `of` is a lowercase token of letters, digits, `_` and
111+ * `-`, which may start with a digit, at most 32 of them; `value` is one of
112+ * them.
113+ */
114+ export type TelemetryChoice = { value: string; of: readonly string[] }
115+}
@@ -13,5 +13,5 @@
1313 "jsxFactory": "h",
1414 "jsxFragmentFactory": "Fragment"
1515 },
16- "include": ["types", "*/hooks", "*/tests"]
16+ "include": ["types", "*/types/**/*.d.ts", "*/hooks", "*/tests"]
1717 }