@getargvio/argvio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,454 @@
1
+ import { Histogram, Counter } from '@opentelemetry/api';
2
+ import { Logger } from '@opentelemetry/api-logs';
3
+ import { LoggerProvider } from '@opentelemetry/sdk-logs';
4
+ import { MeterProvider } from '@opentelemetry/sdk-metrics';
5
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
6
+
7
+ /**
8
+ * The consent tier a CLI user (or their organization) has granted for
9
+ * telemetry collection. Gates both which `record*` calls are allowed to
10
+ * emit which fields, and the value sent on the `cli.analytics.tier`
11
+ * resource attribute, which the public ingest server re-validates and
12
+ * enforces as a backstop.
13
+ *
14
+ * Tiers are strictly ordered: each tier is a superset of the fields
15
+ * allowed at the tier below it. Compare with plain `<`/`<=`/`>=` — the
16
+ * numeric values are significant, not just distinct enum tags.
17
+ *
18
+ * TODO(schema): the wire string values in {@link tierToString} are
19
+ * provisional pending the server-side allowlist file being confirmed.
20
+ * They MUST match the `public` server's semantic allowlist exactly, or
21
+ * the server will reject/strip the resource attribute.
22
+ */
23
+ declare const Tier: {
24
+ /** No persistent or cross-invocation identifiers; aggregate counts only. */
25
+ readonly Anonymous: 0;
26
+ /** Adds command path, flag names (not values), exit code, and latency. */
27
+ readonly Basic: 1;
28
+ /** Adds session correlation and classified (not raw) error signatures. */
29
+ readonly Full: 2;
30
+ /**
31
+ * The explicit opt-in tier: raw error detail (stack traces, messages)
32
+ * and any other field the allowlist marks opt-in-only. Never the
33
+ * default; a vendor or user must positively select it.
34
+ */
35
+ readonly OptIn: 3;
36
+ };
37
+ type Tier = (typeof Tier)[keyof typeof Tier];
38
+ /** The OTel resource attribute key the tier is reported under. */
39
+ declare const RESOURCE_ATTR_TIER = "cli.analytics.tier";
40
+ /** Returns the wire value sent on the `cli.analytics.tier` resource attribute. */
41
+ declare function tierToString(tier: Tier): string;
42
+ /**
43
+ * Parses a wire tier string (as produced by {@link tierToString}) back
44
+ * into a {@link Tier}. Returns `undefined` if `value` does not match a
45
+ * known tier — callers should fail closed to `Tier.Anonymous` in that
46
+ * case, never assume the highest tier.
47
+ */
48
+ declare function parseTier(value: string): Tier | undefined;
49
+ /** Reports whether `value` is a known {@link Tier}. */
50
+ declare function isValidTier(value: number): value is Tier;
51
+ /**
52
+ * Reports whether `tier` permits emitting a field scoped to `required`.
53
+ * A session resolved to `Tier.Full`, for example, `tierAllows(Tier.Full,
54
+ * Tier.Basic)` and `tierAllows(Tier.Full, Tier.Full)` but not
55
+ * `tierAllows(Tier.Full, Tier.OptIn)`.
56
+ */
57
+ declare function tierAllows(tier: Tier, required: Tier): boolean;
58
+ /** Returns the lower of `tier` and `ceiling`. */
59
+ declare function clampTier(tier: Tier, ceiling: Tier): Tier;
60
+ /**
61
+ * A coarse, vendor-assigned classification of an error — deliberately
62
+ * not the error's message or type name, both of which can carry
63
+ * arbitrary (and potentially sensitive) free-form text. This SDK does
64
+ * not attempt to infer a category from an error value itself; the
65
+ * vendor's own error handling already knows which of these applies.
66
+ */
67
+ declare const ErrorCategory: {
68
+ readonly Unknown: "unknown";
69
+ /** Bad input, invalid flags/args. */
70
+ readonly User: "user";
71
+ readonly Validation: "validation";
72
+ readonly Network: "network";
73
+ readonly Auth: "auth";
74
+ readonly Internal: "internal";
75
+ readonly Timeout: "timeout";
76
+ readonly Canceled: "canceled";
77
+ };
78
+ type ErrorCategory = (typeof ErrorCategory)[keyof typeof ErrorCategory];
79
+ declare function isValidErrorCategory(value: string): value is ErrorCategory;
80
+
81
+ /**
82
+ * Resolves the consent tier that should be used for the current
83
+ * process. Consulted once per {@link Client}, synchronously, at
84
+ * construction time (cached for the process lifetime — consent is not
85
+ * expected to change mid-invocation of a short-lived CLI command).
86
+ *
87
+ * `resolve` here is **synchronous**: Node's single-threaded event loop
88
+ * has no way to synchronously await a Promise, and `createClient` must
89
+ * return a fully
90
+ * resolved, usable Client without an async round-trip (see the "why
91
+ * createClient is synchronous" note in client.ts). Implementations must
92
+ * therefore be fast and local only — env vars, an already-loaded config
93
+ * object, a synchronous local file read — never network I/O. A provider
94
+ * that throws is treated identically to `Tier.Anonymous` (fail closed).
95
+ */
96
+ interface ConsentProvider {
97
+ resolve(): Tier;
98
+ }
99
+ /**
100
+ * A {@link ConsentProvider} that always resolves to a fixed tier. Useful
101
+ * for tests, for vendors who resolve consent themselves before
102
+ * constructing the Client, or as a fallback wrapped by
103
+ * {@link chainProviders}.
104
+ */
105
+ declare class StaticProvider implements ConsentProvider {
106
+ private readonly tier;
107
+ constructor(tier: Tier);
108
+ resolve(): Tier;
109
+ }
110
+ /**
111
+ * A {@link ConsentProvider} that always resolves to `Tier.Anonymous`. It
112
+ * exists as an explicit, self-documenting choice distinct from "a
113
+ * provider that happens to return anonymous".
114
+ */
115
+ declare class DisabledProvider implements ConsentProvider {
116
+ resolve(): Tier;
117
+ }
118
+ /**
119
+ * Returns a {@link ConsentProvider} that tries each provider in order
120
+ * and returns the first successful (non-throwing) result. If all
121
+ * providers throw, it throws {@link errNoProviderResolved} (the caller
122
+ * treats that as `Tier.Anonymous`, same as any other thrown error). Lets
123
+ * a vendor compose, e.g., "env override, then config file, then
124
+ * anonymous default" without writing that logic themselves.
125
+ */
126
+ declare function chainProviders(...providers: ReadonlyArray<ConsentProvider | null | undefined>): ConsentProvider;
127
+
128
+ type Transport = 'http' | 'grpc';
129
+ /**
130
+ * The hosted public ingest endpoint for OTLP/HTTP. Overridden by
131
+ * `options.endpoint` for self-hosted/on-prem deployments.
132
+ *
133
+ * TODO(schema): confirm the production hostname; this is a placeholder.
134
+ */
135
+ declare const DEFAULT_ENDPOINT_HTTP = "https://ingest.argvio.io:4318";
136
+ /** The hosted public ingest endpoint for OTLP/gRPC. Same TODO(schema) status as above. */
137
+ declare const DEFAULT_ENDPOINT_GRPC = "ingest.argvio.io:4317";
138
+ /**
139
+ * The metadata/header key the API key is sent under.
140
+ *
141
+ * TODO(schema): confirm against the public server's actual auth
142
+ * contract; placeholder pending that being pasted into this repo.
143
+ */
144
+ declare const DEFAULT_AUTH_HEADER = "x-argvio-api-key";
145
+ declare const DEFAULT_EXPORT_TIMEOUT_MS = 10000;
146
+ declare const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
147
+ declare const DEFAULT_QUEUE_SIZE = 2048;
148
+ /** Configures a {@link Client} constructed by `createClient`. */
149
+ interface ClientOptions {
150
+ /**
151
+ * Overrides the OTLP collector endpoint. Defaults to the hosted
152
+ * public endpoint appropriate for `transport`.
153
+ */
154
+ endpoint?: string;
155
+ /**
156
+ * Selects the OTLP wire transport. Defaults to `'http'` so a CLI that
157
+ * embeds it doesn't pull in a gRPC client stack by default. Pass
158
+ * `'grpc'` if you need gRPC.
159
+ */
160
+ transport?: Transport;
161
+ /** Disables transport security. Only intended for local development against a self-hosted collector without TLS. */
162
+ insecure?: boolean;
163
+ /** Overrides the header the API key is sent under. Rarely needed. */
164
+ authHeader?: string;
165
+ /** The consent tier used when no `consentProvider` is supplied, or if it rejects/times out. Defaults to `Tier.Anonymous`. */
166
+ defaultTier?: Tier;
167
+ /** Resolves the session's consent tier at construction time. If unset, `defaultTier` is used directly. */
168
+ consentProvider?: ConsentProvider;
169
+ /**
170
+ * Fully disables the client: `createClient` returns a Client that
171
+ * performs zero network calls and whose `record*` methods are no-ops
172
+ * with negligible overhead. Equivalent to setting `ARGVIO_DISABLED=1`,
173
+ * and also engaged automatically when `DO_NOT_TRACK` requests it.
174
+ */
175
+ disabled?: boolean;
176
+ /** Bounds how long a single export attempt may take. */
177
+ exportTimeoutMs?: number;
178
+ /** Bounds how long `flush`/`shutdown` may block waiting for in-flight data to be exported. */
179
+ shutdownTimeoutMs?: number;
180
+ /** Bounds the number of pending records buffered before new records are dropped (counted, never blocking). */
181
+ queueSize?: number;
182
+ /**
183
+ * Additional static OTel resource attributes (e.g. a build channel or
184
+ * distribution identifier). Not tier-gated and not part of the
185
+ * taxonomy — use sparingly, for attributes that describe the CLI
186
+ * build itself rather than any individual invocation.
187
+ */
188
+ resourceAttributes?: Record<string, string>;
189
+ }
190
+
191
+ /** A snapshot of in-process telemetry-about-telemetry: never sent over the network. */
192
+ interface Stats {
193
+ /** Records dropped because a batch queue was full rather than blocking the host CLI. */
194
+ queueOverflowDropped: number;
195
+ /** Records/fields dropped because the resolved consent tier didn't permit them. */
196
+ belowTierDropped: number;
197
+ /** Internal errors recovered from; should always be zero. */
198
+ recoveredErrors: number;
199
+ }
200
+
201
+ interface ClientParams {
202
+ disabled: boolean;
203
+ initError: Error | null;
204
+ tier: Tier;
205
+ queueSize: number;
206
+ shutdownTimeoutMs: number;
207
+ tracerProvider?: NodeTracerProvider;
208
+ meterProvider?: MeterProvider;
209
+ loggerProvider?: LoggerProvider;
210
+ logger?: Logger;
211
+ latencyHist?: Histogram;
212
+ invocationsCounter?: Counter;
213
+ sessionsCounter?: Counter;
214
+ errorEventsCounter?: Counter;
215
+ }
216
+ /**
217
+ * The entry point for emitting telemetry. Every method is safe to call
218
+ * on a disabled Client (explicitly disabled, `DO_NOT_TRACK`/`ARGVIO_DISABLED`,
219
+ * or degraded due to a construction error) — vendors never need to
220
+ * check `disabled` before using a Client.
221
+ *
222
+ * Construct via {@link createClient}, never directly.
223
+ */
224
+ declare class Client {
225
+ readonly tier: Tier;
226
+ readonly disabled: boolean;
227
+ /** Set if construction encountered a problem; the Client still degrades to a safe disabled no-op. */
228
+ readonly initError: Error | null;
229
+ private readonly counters;
230
+ private readonly queue;
231
+ private readonly shutdownTimeoutMs;
232
+ private shuttingDown;
233
+ private shutdownStarted;
234
+ private sessionId;
235
+ private readonly tracerProvider;
236
+ private readonly meterProvider;
237
+ private readonly loggerProvider;
238
+ private readonly logger;
239
+ private readonly latencyHist;
240
+ private readonly invocationsCounter;
241
+ private readonly sessionsCounter;
242
+ private readonly errorEventsCounter;
243
+ /** @internal use {@link createClient} */
244
+ constructor(params: ClientParams);
245
+ private enabled;
246
+ /** A snapshot of in-process counters about dropped records. Never sent over the network. */
247
+ stats(): Stats;
248
+ private gate;
249
+ private tierAttr;
250
+ private emitEvent;
251
+ /**
252
+ * Lazily generates a session correlation ID at `Tier.Full`+. A random,
253
+ * non-persistent identifier scoped to a single process invocation —
254
+ * not a user or device identifier.
255
+ */
256
+ private ensureSessionId;
257
+ /**
258
+ * Records the start of a CLI process session. Call once, as early as
259
+ * possible. At `Tier.Anonymous` this only increments an aggregate
260
+ * session counter. At `Tier.Basic`+ it also emits a `cli.session_start`
261
+ * event. At `Tier.Full`+ the event carries a per-process session
262
+ * correlation ID.
263
+ */
264
+ recordSessionStart(): void;
265
+ /** Records the end of a CLI process session. Call once, symmetric with {@link recordSessionStart}. */
266
+ recordSessionEnd(): void;
267
+ /**
268
+ * Records that a command began executing. `commandPath` should be the
269
+ * full command path (e.g. `"mycli sub subsub"`), not just the leaf
270
+ * command name. `flagNames` is the list of flag *names* the user set —
271
+ * never flag values, which this SDK never accepts anywhere in the
272
+ * taxonomy.
273
+ *
274
+ * At `Tier.Anonymous` this only increments an aggregate invocation
275
+ * counter with no path/flag detail. At `Tier.Basic`+ the full command
276
+ * path and flag names are included.
277
+ */
278
+ recordCommandInvocation(commandPath: string, flagNames?: readonly string[]): void;
279
+ /**
280
+ * Records the exit code a command finished with. Pass `""` for
281
+ * `commandPath` if unknown (e.g. a single-command CLI). At
282
+ * `Tier.Anonymous` the exit code is recorded without a command path.
283
+ * At `Tier.Basic`+ the command path is included.
284
+ */
285
+ recordExitCode(commandPath: string, code: number): void;
286
+ /**
287
+ * Records how long a command took to execute, in milliseconds, on a
288
+ * histogram (`cli.command.duration`). Available at every tier.
289
+ */
290
+ recordLatency(commandPath: string, durationMs: number): void;
291
+ /** Records that a command's help output was requested (`-h`/`--help` or an explicit help command). */
292
+ recordHelpFlagUsage(commandPath: string): void;
293
+ /**
294
+ * Records that a command failed with an error, classified by
295
+ * `category` rather than its raw message or stack trace — this is
296
+ * deliberately the only error-recording capability available below
297
+ * `Tier.OptIn`. Raw error detail is only reachable via {@link optIn}.
298
+ *
299
+ * Requires `Tier.Full`+; at lower tiers this is a no-op (counted in
300
+ * `stats().belowTierDropped`).
301
+ */
302
+ recordError(commandPath: string, category?: ErrorCategory): void;
303
+ /**
304
+ * Returns an {@link OptInScope} if, and only if, this Client's resolved
305
+ * consent tier is `Tier.OptIn`; otherwise returns `undefined`. There is
306
+ * no other way to reach raw error detail (message, stack trace)
307
+ * anywhere in this package — it's structurally impossible to pass that
308
+ * data through {@link recordError}'s signature.
309
+ */
310
+ optIn(): OptInScope | undefined;
311
+ /** @internal called only by {@link OptInScope}. */
312
+ _recordErrorDetail(commandPath: string, err: Error, stack?: string): void;
313
+ /**
314
+ * Blocks until all telemetry queued so far has been handed to the
315
+ * exporters and an export attempt has completed, or `timeoutMs`
316
+ * elapses, whichever comes first. Safe to call on a disabled Client
317
+ * (a no-op).
318
+ */
319
+ flush(timeoutMs?: number): Promise<void>;
320
+ /**
321
+ * Flushes any queued telemetry (best-effort, bounded by `timeoutMs`)
322
+ * and releases all background resources. After `shutdown`, the Client
323
+ * permanently behaves as a disabled no-op. Safe to call on a disabled
324
+ * Client, and safe to call more than once.
325
+ */
326
+ shutdown(timeoutMs?: number): Promise<void>;
327
+ }
328
+ /**
329
+ * Exposes telemetry capabilities that require the user's explicit
330
+ * `Tier.OptIn` consent, structurally separated from {@link Client} so
331
+ * that raw error content (messages, stack traces) can never be passed
332
+ * through a lower-tier code path by mistake or misuse.
333
+ */
334
+ declare class OptInScope {
335
+ private readonly client;
336
+ constructor(client: Client);
337
+ /** Records an error's message at full fidelity. */
338
+ recordErrorDetail(commandPath: string, err: Error): void;
339
+ /** Identical to {@link recordErrorDetail} but also attaches a raw stack trace. */
340
+ recordErrorDetailWithStack(commandPath: string, err: Error, stack: string): void;
341
+ }
342
+ /**
343
+ * Constructs a {@link Client}. `apiKey`, `cliName`, and `cliVersion` are
344
+ * always required; everything else has a sane default and is set via
345
+ * `options`.
346
+ *
347
+ * `createClient` is synchronous and **never throws**: if construction
348
+ * encounters a problem (e.g. the exporter transport can't be built, or a
349
+ * `ConsentProvider` throws), it returns a Client already degraded to a
350
+ * disabled no-op, with `client.initError` describing why. Callers may
351
+ * inspect `initError` but are not required to before using the returned
352
+ * Client safely.
353
+ *
354
+ * `createClient` resolves consent through a synchronous
355
+ * {@link ConsentProvider} — see that interface's doc comment for why
356
+ * resolution has to be synchronous on Node's single-threaded event loop.
357
+ */
358
+ declare function createClient(apiKey: string, cliName: string, cliVersion: string, options?: ClientOptions): Client;
359
+
360
+ /** Reports whether the current process appears to be running inside a CI system. */
361
+ declare function isCI(): boolean;
362
+ /**
363
+ * Reports whether the `DO_NOT_TRACK` environment variable
364
+ * (https://do-not-track.dev) requests telemetry be disabled. Any value
365
+ * other than unset, empty, "0", or "false" is treated as a request.
366
+ */
367
+ declare function isDoNotTrackRequested(): boolean;
368
+ /**
369
+ * A {@link ConsentProvider} that caps the resolved tier at
370
+ * `Tier.Anonymous` when the process appears to be running in a CI
371
+ * environment. It resolves `Tier.Anonymous` in that case and rejects
372
+ * otherwise, so it's meant to be placed ahead of other providers in a
373
+ * {@link chainProviders} call — it "wins" only when CI is detected,
374
+ * deferring to the next provider otherwise.
375
+ *
376
+ * Reasoning: CI runs are usually unattended, share no single human
377
+ * user's stored consent preference, and can run at high volume (every
378
+ * commit, every matrix leg). We still allow anonymous-tier telemetry
379
+ * (e.g. install/build counts) by default rather than fully disabling,
380
+ * because that data has real product value and carries no identifying
381
+ * fields at `Tier.Anonymous` by construction. Vendors who want CI runs
382
+ * fully silent can pass `disabled: true` or check `isCI()` themselves.
383
+ *
384
+ * `DO_NOT_TRACK` is handled separately (see {@link isDoNotTrackRequested})
385
+ * and is enforced by the Client as a full disable, not a tier cap: it's
386
+ * a standing, cross-tool opt-out signal a user or environment sets
387
+ * deliberately, so it's honored as "no telemetry" rather than "least
388
+ * telemetry".
389
+ */
390
+ declare class EnvConsentProvider implements ConsentProvider {
391
+ resolve(): Tier;
392
+ }
393
+
394
+ interface FileConsentProviderOptions {
395
+ /** Overrides the default config file location entirely. */
396
+ path?: string;
397
+ }
398
+ /**
399
+ * The reference {@link ConsentProvider} implementation: reads a small
400
+ * JSON file at a conventional per-CLI config location, namespaced under
401
+ * the vendor's own CLI name to avoid collisions between unrelated CLIs
402
+ * that both embed this SDK.
403
+ *
404
+ * Reads/writes are synchronous (see {@link ConsentProvider}'s doc
405
+ * comment for why) — a small local JSON file read is cheap enough that
406
+ * this doesn't meaningfully block startup.
407
+ *
408
+ * Use `options.path` to override the location entirely (e.g. a vendor
409
+ * that already has its own config file and wants to store the tier
410
+ * alongside it under a different mechanism should implement their own
411
+ * {@link ConsentProvider} instead).
412
+ */
413
+ declare class FileConsentProvider implements ConsentProvider {
414
+ /** The resolved config file path (after defaults/overrides), primarily for diagnostics and tests. */
415
+ readonly path: string | undefined;
416
+ constructor(cliName: string, options?: FileConsentProviderOptions);
417
+ /**
418
+ * A missing file, unreadable file, or malformed contents all throw
419
+ * synchronously, so callers should place `FileConsentProvider` inside
420
+ * `chainProviders` with a sane fallback (e.g.
421
+ * `new StaticProvider(Tier.Anonymous)`).
422
+ */
423
+ resolve(): Tier;
424
+ /**
425
+ * Persists `tier` as the chosen consent tier, creating the parent
426
+ * directory if needed. Intended for use by a vendor's first-run
427
+ * consent prompt. Does not itself enforce any tier ceiling — it's the
428
+ * vendor's own UX writing the user's explicit choice.
429
+ */
430
+ store(tier: Tier): void;
431
+ }
432
+
433
+ /**
434
+ * Attribute keys used on emitted signals. Exported so vendors and tests
435
+ * can assert on them without relying on string literals staying in
436
+ * sync.
437
+ *
438
+ * TODO(schema): these keys are provisional and must be checked against
439
+ * the authoritative server-side allowlist before v0.1.0 ships.
440
+ */
441
+ declare const AttrCommandPath = "cli.command.path";
442
+ declare const AttrCommandFlags = "cli.command.flags";
443
+ declare const AttrExitCode = "cli.command.exit_code";
444
+ declare const AttrHelpUsed = "cli.command.help_used";
445
+ declare const AttrSessionId = "cli.session.id";
446
+ declare const AttrErrorCategory = "cli.error.category";
447
+ /** TierOptIn only. */
448
+ declare const AttrErrorMessage = "cli.error.message";
449
+ /** TierOptIn only. */
450
+ declare const AttrErrorStack = "cli.error.stack";
451
+ /** The name of the histogram instrument `recordLatency` records to (unit: milliseconds). */
452
+ declare const MetricCommandDuration = "cli.command.duration";
453
+
454
+ export { AttrCommandFlags, AttrCommandPath, AttrErrorCategory, AttrErrorMessage, AttrErrorStack, AttrExitCode, AttrHelpUsed, AttrSessionId, Client, type ClientOptions, type ConsentProvider, DEFAULT_AUTH_HEADER, DEFAULT_ENDPOINT_GRPC, DEFAULT_ENDPOINT_HTTP, DEFAULT_EXPORT_TIMEOUT_MS, DEFAULT_QUEUE_SIZE, DEFAULT_SHUTDOWN_TIMEOUT_MS, DisabledProvider, EnvConsentProvider, ErrorCategory, FileConsentProvider, type FileConsentProviderOptions, MetricCommandDuration, OptInScope, RESOURCE_ATTR_TIER, StaticProvider, type Stats, Tier, type Transport, chainProviders, clampTier, createClient, isCI, isDoNotTrackRequested, isValidErrorCategory, isValidTier, parseTier, tierAllows, tierToString };