mirror of
https://github.com/anthropics/claude-code.git
synced 2026-10-02 05:25:05 +08:00
mods: a noun's types live in its own types folder and dependents read them from there
This commit is contained in:
+30
-1
@@ -76,7 +76,36 @@ crosses it. `$.ui.press({ plugin, key })` presses a `Button` the test
|
||||
rendered, as a click in the terminal does.
|
||||
|
||||
`tsc -p mods/tsconfig.json` typechecks every mod's hooks and tests against
|
||||
`types/`.
|
||||
`types/` and each mod's own `types/` contract.
|
||||
|
||||
## Composing mods: noun contracts
|
||||
|
||||
A mod that adds a noun to `$` in the `engine.create` fold owns that noun's
|
||||
types, and keeps them in one place: its `types/index.d.ts`, an ambient file
|
||||
with no imports that merges into `claude-code`, declaring the noun on
|
||||
`EngineInterface` and exporting the types it is made of, each named for the
|
||||
noun (`telemetry/types/index.d.ts` declares `$.telemetry` and exports
|
||||
`Telemetry`, `TelemetryLogEntry`, `TelemetryMarkEntry` and the rest).
|
||||
|
||||
- The contract is the only declaration of the noun. The mod's own hooks
|
||||
import its types from `claude-code` (`import type { Telemetry } from
|
||||
'claude-code'`), and the value its `engine.create` hook returns is checked
|
||||
against `EngineInterface['telemetry']`, so the implementation cannot drift
|
||||
from what callers read.
|
||||
- A mod that calls another's noun reads the same file and never copies it:
|
||||
`mods/tsconfig.json` includes `*/types/**/*.d.ts`, so `$.telemetry.log(…)`
|
||||
in `diff` types against `telemetry`'s contract as it stands.
|
||||
- A test of a mod that calls another's noun seats a provider for it, an inline
|
||||
plugin whose `engine.create` hook adds the noun, and answers the calls the
|
||||
way it answers the engine's: `on('telemetry.log', ($, e) => ({ value:
|
||||
undefined }))` runs above the provider's own method, its `e` typed by the
|
||||
contract. With no provider loaded the `$` build refuses the hook, naming the
|
||||
noun nobody provides.
|
||||
|
||||
A plugin outside this repository that depends on a mod's noun points its
|
||||
tsconfig `include` at that mod's `types/` folder for now; once the engine
|
||||
writes the contracts of the plugins a session has installed, `/plugin-types`
|
||||
will put them beside `claude-code.d.ts` and the include goes away.
|
||||
|
||||
Early access: hooks modules load only where function hooks are enabled, and
|
||||
the API these mods are written against may change between releases without
|
||||
|
||||
Vendored
-57
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* The `$.telemetry` noun as this plugin calls it, declared for a build of
|
||||
* this folder on its own; no module imports it.
|
||||
*
|
||||
* The telemetry plugin adds the noun in the engine.create fold on internal
|
||||
* builds and nowhere else: where no plugin provides it the calls throw and
|
||||
* record/ drops the row. The engine's repository leaves this file out.
|
||||
*
|
||||
* @entry
|
||||
*/
|
||||
declare module 'claude-code' {
|
||||
/**
|
||||
* How one use of a feature went: as hoped, degraded, or failed outright.
|
||||
*/
|
||||
type DiffTelemetryMarkKind = 'ok' | 'sad' | 'bad'
|
||||
|
||||
/**
|
||||
* A string property: the value and the list it is chosen from.
|
||||
*/
|
||||
type DiffTelemetryChoice = { value: string; of: readonly string[] }
|
||||
|
||||
/**
|
||||
* One property value a row may carry.
|
||||
*/
|
||||
type DiffTelemetryProp = number | boolean | DiffTelemetryChoice
|
||||
|
||||
/**
|
||||
* What `$.telemetry.log` takes: the event's name after the prefix, and
|
||||
* its properties by snake_case key.
|
||||
*/
|
||||
type DiffTelemetryLogEntry = {
|
||||
event: string
|
||||
props?: Readonly<Record<string, DiffTelemetryProp>>
|
||||
}
|
||||
|
||||
/**
|
||||
* What `$.telemetry.mark` takes: the feature, how it went, why when not
|
||||
* ok, and the properties the row carries beside them by snake_case key.
|
||||
*/
|
||||
type DiffTelemetryMarkEntry = {
|
||||
feature: string
|
||||
kind: DiffTelemetryMarkKind
|
||||
reason?: string
|
||||
props?: Readonly<Record<string, DiffTelemetryProp>>
|
||||
}
|
||||
|
||||
interface EngineInterface {
|
||||
/**
|
||||
* A plugin's analytics, one first-party row per call; present only
|
||||
* where the telemetry plugin is seated.
|
||||
*/
|
||||
telemetry: {
|
||||
log: (entry: DiffTelemetryLogEntry) => Promise<void>
|
||||
mark: (entry: DiffTelemetryMarkEntry) => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,6 @@
|
||||
"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.",
|
||||
"author": {
|
||||
"name": "Anthropic"
|
||||
}
|
||||
},
|
||||
"types": "types/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ string named together with the list it is chosen from), under `log` and
|
||||
the last two and refused on the first. An entry that breaks a rule is
|
||||
refused before anything is sent.
|
||||
|
||||
`hooks/register.ts` is the module; `hooks/telemetry-types/` is the noun's
|
||||
type as a caller sees it.
|
||||
`hooks/register.ts` is the module; `types/index.d.ts` is the noun's contract,
|
||||
the one declaration of `$.telemetry` that this mod's hooks, a mod calling the
|
||||
noun and a test answering it all read.
|
||||
|
||||
## What it hooks
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type TelemetryTypes from '../../telemetry-types'
|
||||
import { checkedValue } from '../checked-value'
|
||||
import { isRecord } from '../is-record'
|
||||
import type { Method } from '../method'
|
||||
import { PROP_LIMIT } from '../prop-limit'
|
||||
import { refusal } from '../refusal'
|
||||
import { TOKEN } from '../token'
|
||||
@@ -15,7 +15,7 @@ import { TOKEN } from '../token'
|
||||
*/
|
||||
export function checkedProps(
|
||||
props: unknown,
|
||||
method: TelemetryTypes.Method,
|
||||
method: Method,
|
||||
): Record<string, string | number | boolean> {
|
||||
if (!isRecord(props)) {
|
||||
throw refusal('props: an object of properties by key', method)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type TelemetryTypes from '../../telemetry-types'
|
||||
import { CHOICE_TOKEN } from '../choice-token'
|
||||
import { CHOICES_LIMIT } from '../choices-limit'
|
||||
import { isRecord } from '../is-record'
|
||||
import type { Method } from '../method'
|
||||
import { refusal } from '../refusal'
|
||||
|
||||
/**
|
||||
@@ -19,7 +19,7 @@ import { refusal } from '../refusal'
|
||||
export function checkedValue(
|
||||
key: string,
|
||||
value: unknown,
|
||||
method: TelemetryTypes.Method = 'log',
|
||||
method: Method = 'log',
|
||||
): string | number | boolean {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from './is-record'
|
||||
export * from './mark'
|
||||
export * from './mark-fields-of'
|
||||
export * from './mark-kinds'
|
||||
export * from './method'
|
||||
export * from './prop-limit'
|
||||
export * from './refusal'
|
||||
export * from './row-session'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type TelemetryTypes from '../../telemetry-types'
|
||||
import type { TelemetryMarkKind } from 'claude-code'
|
||||
|
||||
import { MARK_KINDS } from '../mark-kinds'
|
||||
|
||||
/**
|
||||
@@ -7,5 +8,5 @@ import { MARK_KINDS } from '../mark-kinds'
|
||||
* @param value what the caller passed as `kind`
|
||||
* @returns whether value names one of the three mark kinds
|
||||
*/
|
||||
export const isMarkKind = (value: unknown): value is TelemetryTypes.MarkKind =>
|
||||
export const isMarkKind = (value: unknown): value is TelemetryMarkKind =>
|
||||
MARK_KINDS.some(kind => kind === value)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type TelemetryTypes from '../../telemetry-types'
|
||||
import type { TelemetryMarkKind } from 'claude-code'
|
||||
|
||||
/**
|
||||
* The three kinds a mark may be, in the order the feature events name them.
|
||||
*/
|
||||
export const MARK_KINDS: readonly TelemetryTypes.MarkKind[] = [
|
||||
'ok',
|
||||
'sad',
|
||||
'bad',
|
||||
]
|
||||
export const MARK_KINDS: readonly TelemetryMarkKind[] = ['ok', 'sad', 'bad']
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type TelemetryTypes from '../../telemetry-types'
|
||||
import type { TelemetryMarkKind } from 'claude-code'
|
||||
|
||||
import type { Fields } from '../fields'
|
||||
|
||||
/**
|
||||
@@ -6,7 +7,7 @@ import type { Fields } from '../fields'
|
||||
* and its properties as they go into the row.
|
||||
*/
|
||||
export type Mark = {
|
||||
kind: TelemetryTypes.MarkKind
|
||||
kind: TelemetryMarkKind
|
||||
feature: string
|
||||
reason?: string
|
||||
props: Fields['props']
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type * from './method.js'
|
||||
|
||||
export * as default from '.'
|
||||
@@ -1,4 +1,4 @@
|
||||
import type TelemetryTypes from '../../telemetry-types'
|
||||
import type { Method } from '../method'
|
||||
|
||||
/**
|
||||
* The error a refused entry rejects with, naming the method and what was wrong.
|
||||
@@ -11,7 +11,5 @@ import type TelemetryTypes from '../../telemetry-types'
|
||||
* @returns the error to reject the call with, naming the method and what was
|
||||
* wrong
|
||||
*/
|
||||
export const refusal = (
|
||||
what: string,
|
||||
method: TelemetryTypes.Method = 'log',
|
||||
): Error => new Error(`$.telemetry.${method}: ${what}`)
|
||||
export const refusal = (what: string, method: Method = 'log'): Error =>
|
||||
new Error(`$.telemetry.${method}: ${what}`)
|
||||
|
||||
@@ -4,6 +4,5 @@ export * from './is-analytics-off'
|
||||
export * from './register.js'
|
||||
export * from './telemetry-deps'
|
||||
export * from './telemetry-of'
|
||||
export * from './telemetry-types'
|
||||
|
||||
export * as default from '.'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { On } from 'claude-code'
|
||||
import type { EngineInterface, On } from 'claude-code'
|
||||
|
||||
import { telemetryOf } from './telemetry-of'
|
||||
|
||||
@@ -15,33 +15,30 @@ export function register(on: On) {
|
||||
on('engine.create', async ($, e, next) => {
|
||||
const beneath = await next(e)
|
||||
|
||||
return {
|
||||
...beneath,
|
||||
telemetry: telemetryOf({
|
||||
authorize: () => beneath.session.authorize(),
|
||||
id: () => beneath.session.id(),
|
||||
model: () => beneath.session.model(),
|
||||
environment: async () => ({
|
||||
userType: await beneath.env.get('USER_TYPE'),
|
||||
disableTelemetry: await beneath.env.get('DISABLE_TELEMETRY'),
|
||||
disableNonessentialTraffic: await beneath.env.get(
|
||||
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
||||
),
|
||||
doNotTrack: await beneath.env.get('DO_NOT_TRACK'),
|
||||
customOauthUrl: await beneath.env.get('CLAUDE_CODE_CUSTOM_OAUTH_URL'),
|
||||
useBedrock: await beneath.env.get('CLAUDE_CODE_USE_BEDROCK'),
|
||||
useVertex: await beneath.env.get('CLAUDE_CODE_USE_VERTEX'),
|
||||
useFoundry: await beneath.env.get('CLAUDE_CODE_USE_FOUNDRY'),
|
||||
useAnthropicAws: await beneath.env.get(
|
||||
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
||||
),
|
||||
useAnthropicGoogleCloud: await beneath.env.get(
|
||||
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
||||
),
|
||||
useMantle: await beneath.env.get('CLAUDE_CODE_USE_MANTLE'),
|
||||
}),
|
||||
fetch: (url, init) => beneath.http.fetch(url, init),
|
||||
const telemetry: EngineInterface['telemetry'] = telemetryOf({
|
||||
authorize: () => beneath.session.authorize(),
|
||||
id: () => beneath.session.id(),
|
||||
model: () => beneath.session.model(),
|
||||
environment: async () => ({
|
||||
userType: await beneath.env.get('USER_TYPE'),
|
||||
disableTelemetry: await beneath.env.get('DISABLE_TELEMETRY'),
|
||||
disableNonessentialTraffic: await beneath.env.get(
|
||||
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
||||
),
|
||||
doNotTrack: await beneath.env.get('DO_NOT_TRACK'),
|
||||
customOauthUrl: await beneath.env.get('CLAUDE_CODE_CUSTOM_OAUTH_URL'),
|
||||
useBedrock: await beneath.env.get('CLAUDE_CODE_USE_BEDROCK'),
|
||||
useVertex: await beneath.env.get('CLAUDE_CODE_USE_VERTEX'),
|
||||
useFoundry: await beneath.env.get('CLAUDE_CODE_USE_FOUNDRY'),
|
||||
useAnthropicAws: await beneath.env.get('CLAUDE_CODE_USE_ANTHROPIC_AWS'),
|
||||
useAnthropicGoogleCloud: await beneath.env.get(
|
||||
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
||||
),
|
||||
useMantle: await beneath.env.get('CLAUDE_CODE_USE_MANTLE'),
|
||||
}),
|
||||
}
|
||||
fetch: (url, init) => beneath.http.fetch(url, init),
|
||||
})
|
||||
|
||||
return { ...beneath, telemetry }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Telemetry } from 'claude-code'
|
||||
|
||||
import Entries from '../entries'
|
||||
import { isAnalyticsOff } from '../is-analytics-off'
|
||||
import type { TelemetryDeps } from '../telemetry-deps'
|
||||
import type TelemetryTypes from '../telemetry-types'
|
||||
|
||||
/**
|
||||
* Builds `$.telemetry`: `log` and `mark` check the entry, read the environment
|
||||
@@ -14,12 +15,12 @@ import type TelemetryTypes from '../telemetry-types'
|
||||
* @param deps the calls on the nouns beneath
|
||||
* @returns the `$.telemetry` interface, `log` and `mark`
|
||||
*/
|
||||
export function telemetryOf(deps: TelemetryDeps): TelemetryTypes.Telemetry {
|
||||
export function telemetryOf(deps: TelemetryDeps): Telemetry {
|
||||
let queue: Promise<unknown> = Promise.resolve()
|
||||
|
||||
async function post(
|
||||
fields: Entries.Fields,
|
||||
method: TelemetryTypes.Method,
|
||||
method: Entries.Method,
|
||||
): Promise<void> {
|
||||
const environment = await deps.environment()
|
||||
|
||||
@@ -59,7 +60,7 @@ export function telemetryOf(deps: TelemetryDeps): TelemetryTypes.Telemetry {
|
||||
|
||||
function queued(
|
||||
fields: Entries.Fields,
|
||||
method: TelemetryTypes.Method,
|
||||
method: Entries.Method,
|
||||
): Promise<void> {
|
||||
const turn = queue.then(() => post(fields, method))
|
||||
queue = turn.catch(() => undefined)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* A string property: the value and the list it is chosen from, declared
|
||||
* beside it, so no free text reaches the row.
|
||||
*
|
||||
* Every member of `of` is a lowercase token of letters, digits, `_` and
|
||||
* `-`, which may start with a digit, at most CHOICES_LIMIT of them; `value`
|
||||
* is one of them.
|
||||
*/
|
||||
export type Choice = { value: string; of: readonly string[] }
|
||||
@@ -1,3 +0,0 @@
|
||||
export type * from './choice.js'
|
||||
|
||||
export * as default from '.'
|
||||
@@ -1,9 +0,0 @@
|
||||
export * from './choice'
|
||||
export * from './log-entry'
|
||||
export * from './mark-entry'
|
||||
export * from './mark-kind'
|
||||
export type * from './method.js'
|
||||
export * from './prop'
|
||||
export type * from './telemetry.js'
|
||||
|
||||
export * as default from '.'
|
||||
@@ -1,3 +0,0 @@
|
||||
export type * from './log-entry.js'
|
||||
|
||||
export * as default from '.'
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { Prop } from '../prop'
|
||||
|
||||
/**
|
||||
* What `$.telemetry.log` takes: the event's name after the prefix, and its
|
||||
* properties by snake_case key.
|
||||
*/
|
||||
export type LogEntry = {
|
||||
event: string
|
||||
props?: Readonly<Record<string, Prop>>
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type * from './mark-entry.js'
|
||||
|
||||
export * as default from '.'
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { MarkKind } from '../mark-kind'
|
||||
import type { Prop } from '../prop'
|
||||
|
||||
/**
|
||||
* What `$.telemetry.mark` takes: the feature, how it went, why when not
|
||||
* ok, and the properties the row carries beside them by snake_case key.
|
||||
*/
|
||||
export type MarkEntry = {
|
||||
feature: string
|
||||
kind: MarkKind
|
||||
reason?: string
|
||||
props?: Readonly<Record<string, Prop>>
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type * from './mark-kind.js'
|
||||
|
||||
export * as default from '.'
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* How a feature went, as the CLI's own feature events count it.
|
||||
*
|
||||
* `ok`: used, the person got what they asked. `sad`: degraded, a fallback
|
||||
* or a partial, the person still got something. `bad`: failed, the person
|
||||
* got nothing.
|
||||
*/
|
||||
export type MarkKind = 'ok' | 'sad' | 'bad'
|
||||
@@ -1,3 +0,0 @@
|
||||
export type * from './prop.js'
|
||||
|
||||
export * as default from '.'
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { Choice } from '../choice'
|
||||
|
||||
/**
|
||||
* A property's value: a finite number, a boolean, or a Choice; never free
|
||||
* text.
|
||||
*/
|
||||
export type Prop = number | boolean | Choice
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { LogEntry } from './log-entry'
|
||||
import type { MarkEntry } from './mark-entry'
|
||||
|
||||
/**
|
||||
* A plugin's analytics, sent one event at a time through `$.telemetry`.
|
||||
*
|
||||
* Internal builds alone: the telemetry built-in adds the noun in the
|
||||
* engine.create fold, so a plugin on an external build, or one where the
|
||||
* built-in is off, finds no `$.telemetry`.
|
||||
*/
|
||||
export type Telemetry = {
|
||||
/**
|
||||
* Sends one event, `tengu_plugin_<event>`, as one first-party row;
|
||||
* resolves once the ingest accepted it.
|
||||
*
|
||||
* The calling built-in names itself in `event`; one already named `tengu_…`
|
||||
* is sent as named. A value is a finite number, a boolean or a Choice; free
|
||||
* text is refused. One input, as every op on `$` takes.
|
||||
*
|
||||
* @param entry the event's name, a snake_case token, and its properties by
|
||||
* snake_case key
|
||||
* @example
|
||||
* await $.telemetry.log({
|
||||
* event: "suggest_learning_survey_answered",
|
||||
* props: {
|
||||
* answer: 2,
|
||||
* page: { value: "ready", of: ["ready", "later"] },
|
||||
* },
|
||||
* })
|
||||
*/
|
||||
log: (entry: LogEntry) => Promise<void>
|
||||
|
||||
/**
|
||||
* Marks one use of a feature as the CLI's own feature events do, one
|
||||
* `tengu_feature_<kind>` row; resolves once the ingest accepted it.
|
||||
*
|
||||
* The row carries `feature_name`, `error_code` on sad or bad (`reason`,
|
||||
* required there and refused on ok) and the entry's `props`, checked as
|
||||
* `log`'s are; it joins the product-wide feature surface, so no prefix.
|
||||
*
|
||||
* @param entry the feature, how it went, why when not ok, and the row's
|
||||
* properties by snake_case key
|
||||
* @example
|
||||
* await $.telemetry.mark({ feature: "learn_page", kind: "ok" })
|
||||
* await $.telemetry.mark({
|
||||
* feature: "learn_page",
|
||||
* kind: "ok",
|
||||
* props: { page: { value: "later", of: ["ready", "later"] } },
|
||||
* })
|
||||
* await $.telemetry.mark({
|
||||
* feature: "learn_page",
|
||||
* kind: "sad",
|
||||
* reason: "blocked",
|
||||
* })
|
||||
*/
|
||||
mark: (entry: MarkEntry) => Promise<void>
|
||||
}
|
||||
+2
-4
@@ -1,6 +1,4 @@
|
||||
import type { CommandRunInput } from 'claude-code'
|
||||
|
||||
import type { LogEntry } from '../../hooks/telemetry-types'
|
||||
import type { CommandRunInput, TelemetryLogEntry } from 'claude-code'
|
||||
|
||||
/**
|
||||
* The command that has the recording plugin log an entry, typed as the
|
||||
@@ -9,7 +7,7 @@ import type { LogEntry } from '../../hooks/telemetry-types'
|
||||
* @param entry what to log
|
||||
* @returns `/record <entry>`
|
||||
*/
|
||||
export const record = (entry: LogEntry): CommandRunInput => ({
|
||||
export const record = (entry: TelemetryLogEntry): CommandRunInput => ({
|
||||
command: 'record',
|
||||
args: JSON.stringify(entry),
|
||||
origin: { kind: 'composer' },
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import type { LogEntry } from '../../hooks/telemetry-types'
|
||||
import type { TelemetryLogEntry } from 'claude-code'
|
||||
|
||||
/**
|
||||
* A survey answered, as a plugin logs it.
|
||||
*
|
||||
* @returns the entry, fresh each call
|
||||
*/
|
||||
export const surveyAnswer = (): LogEntry => ({
|
||||
export const surveyAnswer = (): TelemetryLogEntry => ({
|
||||
event: 'survey_answered',
|
||||
props: { answer: 2, seen: true },
|
||||
})
|
||||
|
||||
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* The `$.telemetry` noun as every caller sees it: the one contract for the
|
||||
* noun, merged into `claude-code` beside the engine's own declarations.
|
||||
*
|
||||
* The telemetry mod adds the noun in the `engine.create` fold and checks its
|
||||
* return against `EngineInterface['telemetry']`; a mod that calls it, and a
|
||||
* test that answers it with `on('telemetry.log', …)`, read the same types by
|
||||
* including this folder in their tsconfig. Nothing here is imported: the file
|
||||
* is ambient, so it merges wherever it is included.
|
||||
*/
|
||||
declare module 'claude-code' {
|
||||
interface EngineInterface {
|
||||
/**
|
||||
* A plugin's analytics, one first-party row per call; present only where
|
||||
* the telemetry mod is seated (internal builds), absent everywhere else.
|
||||
*/
|
||||
telemetry: Telemetry
|
||||
}
|
||||
|
||||
/**
|
||||
* A plugin's analytics, sent one event at a time through `$.telemetry`.
|
||||
*
|
||||
* Internal builds alone: the telemetry mod adds the noun in the
|
||||
* `engine.create` fold, so a plugin on an external build, or one where the
|
||||
* mod is off, finds no `$.telemetry` and its call throws.
|
||||
*/
|
||||
export type Telemetry = {
|
||||
/**
|
||||
* Sends one event, `tengu_plugin_<event>`, as one first-party row;
|
||||
* resolves once the ingest accepted it.
|
||||
*
|
||||
* The calling mod names itself in `event`; one already named `tengu_…` is
|
||||
* sent as named. A value is a finite number, a boolean or a
|
||||
* TelemetryChoice; free text is refused. One input, as every op on `$`
|
||||
* takes.
|
||||
*
|
||||
* @param entry the event's name, a snake_case token, and its properties by
|
||||
* snake_case key
|
||||
* @example
|
||||
* await $.telemetry.log({
|
||||
* event: "suggest_learning_survey_answered",
|
||||
* props: {
|
||||
* answer: 2,
|
||||
* page: { value: "ready", of: ["ready", "later"] },
|
||||
* },
|
||||
* })
|
||||
*/
|
||||
log: (entry: TelemetryLogEntry) => Promise<void>
|
||||
|
||||
/**
|
||||
* Marks one use of a feature as the CLI's own feature events do, one
|
||||
* `tengu_feature_<kind>` row; resolves once the ingest accepted it.
|
||||
*
|
||||
* The row carries `feature_name`, `error_code` on sad or bad (`reason`,
|
||||
* required there and refused on ok) and the entry's `props`, checked as
|
||||
* `log`'s are; it joins the product-wide feature surface, so no prefix.
|
||||
*
|
||||
* @param entry the feature, how it went, why when not ok, and the row's
|
||||
* properties by snake_case key
|
||||
* @example
|
||||
* await $.telemetry.mark({ feature: "learn_page", kind: "ok" })
|
||||
* await $.telemetry.mark({
|
||||
* feature: "learn_page",
|
||||
* kind: "sad",
|
||||
* reason: "blocked",
|
||||
* })
|
||||
*/
|
||||
mark: (entry: TelemetryMarkEntry) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* What `$.telemetry.log` takes: the event's name after the prefix, and its
|
||||
* properties by snake_case key.
|
||||
*/
|
||||
export type TelemetryLogEntry = {
|
||||
event: string
|
||||
props?: Readonly<Record<string, TelemetryProp>>
|
||||
}
|
||||
|
||||
/**
|
||||
* What `$.telemetry.mark` takes: the feature, how it went, why when not
|
||||
* ok, and the properties the row carries beside them by snake_case key.
|
||||
*/
|
||||
export type TelemetryMarkEntry = {
|
||||
feature: string
|
||||
kind: TelemetryMarkKind
|
||||
reason?: string
|
||||
props?: Readonly<Record<string, TelemetryProp>>
|
||||
}
|
||||
|
||||
/**
|
||||
* How a feature went, as the CLI's own feature events count it.
|
||||
*
|
||||
* `ok`: used, the person got what they asked. `sad`: degraded, a fallback
|
||||
* or a partial, the person still got something. `bad`: failed, the person
|
||||
* got nothing.
|
||||
*/
|
||||
export type TelemetryMarkKind = 'ok' | 'sad' | 'bad'
|
||||
|
||||
/**
|
||||
* A property's value: a finite number, a boolean, or a TelemetryChoice;
|
||||
* never free text.
|
||||
*/
|
||||
export type TelemetryProp = number | boolean | TelemetryChoice
|
||||
|
||||
/**
|
||||
* A string property: the value and the list it is chosen from, declared
|
||||
* beside it, so no free text reaches the row.
|
||||
*
|
||||
* Every member of `of` is a lowercase token of letters, digits, `_` and
|
||||
* `-`, which may start with a digit, at most 32 of them; `value` is one of
|
||||
* them.
|
||||
*/
|
||||
export type TelemetryChoice = { value: string; of: readonly string[] }
|
||||
}
|
||||
+1
-1
@@ -13,5 +13,5 @@
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment"
|
||||
},
|
||||
"include": ["types", "*/hooks", "*/tests"]
|
||||
"include": ["types", "*/types/**/*.d.ts", "*/hooks", "*/tests"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user