@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.
package/dist/index.js ADDED
@@ -0,0 +1,785 @@
1
+ import { SeverityNumber } from '@opentelemetry/api-logs';
2
+ import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
3
+ import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
4
+ import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
5
+ import { Metadata } from '@grpc/grpc-js';
6
+ import { OTLPTraceExporter as OTLPTraceExporter$1 } from '@opentelemetry/exporter-trace-otlp-http';
7
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
8
+ import { OTLPMetricExporter as OTLPMetricExporter$1 } from '@opentelemetry/exporter-metrics-otlp-http';
9
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
10
+ import { OTLPLogExporter as OTLPLogExporter$1 } from '@opentelemetry/exporter-logs-otlp-http';
11
+ import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
12
+ import { readFileSync, mkdirSync, writeFileSync } from 'fs';
13
+ import { resourceFromAttributes } from '@opentelemetry/resources';
14
+ import { ATTR_SERVICE_VERSION, ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
15
+ import { randomBytes } from 'crypto';
16
+ import { homedir } from 'os';
17
+ import { dirname, join } from 'path';
18
+
19
+ // src/client.ts
20
+
21
+ // src/tier.ts
22
+ var Tier = {
23
+ /** No persistent or cross-invocation identifiers; aggregate counts only. */
24
+ Anonymous: 0,
25
+ /** Adds command path, flag names (not values), exit code, and latency. */
26
+ Basic: 1,
27
+ /** Adds session correlation and classified (not raw) error signatures. */
28
+ Full: 2,
29
+ /**
30
+ * The explicit opt-in tier: raw error detail (stack traces, messages)
31
+ * and any other field the allowlist marks opt-in-only. Never the
32
+ * default; a vendor or user must positively select it.
33
+ */
34
+ OptIn: 3
35
+ };
36
+ var RESOURCE_ATTR_TIER = "cli.analytics.tier";
37
+ var TIER_STRINGS = {
38
+ [Tier.Anonymous]: "anonymous",
39
+ [Tier.Basic]: "basic",
40
+ [Tier.Full]: "full",
41
+ [Tier.OptIn]: "opt-in"
42
+ };
43
+ function tierToString(tier) {
44
+ return TIER_STRINGS[tier] ?? "unknown";
45
+ }
46
+ function parseTier(value) {
47
+ switch (value) {
48
+ case "anonymous":
49
+ return Tier.Anonymous;
50
+ case "basic":
51
+ return Tier.Basic;
52
+ case "full":
53
+ return Tier.Full;
54
+ case "opt-in":
55
+ return Tier.OptIn;
56
+ default:
57
+ return void 0;
58
+ }
59
+ }
60
+ function isValidTier(value) {
61
+ return value === Tier.Anonymous || value === Tier.Basic || value === Tier.Full || value === Tier.OptIn;
62
+ }
63
+ function tierAllows(tier, required) {
64
+ return tier >= required;
65
+ }
66
+ function clampTier(tier, ceiling) {
67
+ return tier > ceiling ? ceiling : tier;
68
+ }
69
+ var ErrorCategory = {
70
+ Unknown: "unknown",
71
+ /** Bad input, invalid flags/args. */
72
+ User: "user",
73
+ Validation: "validation",
74
+ Network: "network",
75
+ Auth: "auth",
76
+ Internal: "internal",
77
+ Timeout: "timeout",
78
+ Canceled: "canceled"
79
+ };
80
+ var ERROR_CATEGORY_VALUES = new Set(Object.values(ErrorCategory));
81
+ function isValidErrorCategory(value) {
82
+ return ERROR_CATEGORY_VALUES.has(value);
83
+ }
84
+
85
+ // src/options.ts
86
+ var DEFAULT_ENDPOINT_HTTP = "https://ingest.argvio.io:4318";
87
+ var DEFAULT_ENDPOINT_GRPC = "ingest.argvio.io:4317";
88
+ var DEFAULT_AUTH_HEADER = "x-argvio-api-key";
89
+ var DEFAULT_EXPORT_TIMEOUT_MS = 1e4;
90
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
91
+ var DEFAULT_QUEUE_SIZE = 2048;
92
+ function resolveConfig(apiKey, cliName, cliVersion, options = {}) {
93
+ const transport = options.transport ?? "http";
94
+ return {
95
+ apiKey,
96
+ cliName,
97
+ cliVersion,
98
+ transport,
99
+ endpoint: options.endpoint || (transport === "grpc" ? DEFAULT_ENDPOINT_GRPC : DEFAULT_ENDPOINT_HTTP),
100
+ insecure: options.insecure ?? false,
101
+ authHeader: options.authHeader || DEFAULT_AUTH_HEADER,
102
+ defaultTier: options.defaultTier ?? Tier.Anonymous,
103
+ consentProvider: options.consentProvider,
104
+ disabled: options.disabled ?? false,
105
+ exportTimeoutMs: options.exportTimeoutMs && options.exportTimeoutMs > 0 ? options.exportTimeoutMs : DEFAULT_EXPORT_TIMEOUT_MS,
106
+ shutdownTimeoutMs: options.shutdownTimeoutMs && options.shutdownTimeoutMs > 0 ? options.shutdownTimeoutMs : DEFAULT_SHUTDOWN_TIMEOUT_MS,
107
+ queueSize: options.queueSize && options.queueSize > 0 ? options.queueSize : DEFAULT_QUEUE_SIZE,
108
+ resourceAttributes: options.resourceAttributes ?? {}
109
+ };
110
+ }
111
+ var SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
112
+ function withScheme(endpoint, insecure) {
113
+ if (!SCHEME_RE.test(endpoint)) {
114
+ return `${insecure ? "http" : "https"}://${endpoint}`;
115
+ }
116
+ return insecure ? endpoint.replace(/^https:\/\//i, "http://") : endpoint;
117
+ }
118
+ function grpcMetadata(cfg) {
119
+ const metadata = new Metadata();
120
+ metadata.set(cfg.authHeader, cfg.apiKey);
121
+ return metadata;
122
+ }
123
+ function httpHeaders(cfg) {
124
+ return { [cfg.authHeader]: cfg.apiKey };
125
+ }
126
+ function buildExporters(cfg) {
127
+ if (cfg.transport === "grpc") {
128
+ const url = withScheme(cfg.endpoint, cfg.insecure);
129
+ const metadata = grpcMetadata(cfg);
130
+ return {
131
+ trace: new OTLPTraceExporter({ url, metadata, timeoutMillis: cfg.exportTimeoutMs }),
132
+ metric: new OTLPMetricExporter({ url, metadata, timeoutMillis: cfg.exportTimeoutMs }),
133
+ log: new OTLPLogExporter({ url, metadata, timeoutMillis: cfg.exportTimeoutMs })
134
+ };
135
+ }
136
+ const base = withScheme(cfg.endpoint, cfg.insecure).replace(/\/+$/, "");
137
+ const headers = httpHeaders(cfg);
138
+ return {
139
+ trace: new OTLPTraceExporter$1({
140
+ url: `${base}/v1/traces`,
141
+ headers,
142
+ timeoutMillis: cfg.exportTimeoutMs
143
+ }),
144
+ metric: new OTLPMetricExporter$1({
145
+ url: `${base}/v1/metrics`,
146
+ headers,
147
+ timeoutMillis: cfg.exportTimeoutMs
148
+ }),
149
+ log: new OTLPLogExporter$1({
150
+ url: `${base}/v1/logs`,
151
+ headers,
152
+ timeoutMillis: cfg.exportTimeoutMs
153
+ })
154
+ };
155
+ }
156
+ var SDK_NAME = "@getargvio/argvio";
157
+ function sdkVersion() {
158
+ try {
159
+ const pkgUrl = new URL("../package.json", import.meta.url);
160
+ const pkg = JSON.parse(readFileSync(pkgUrl, "utf8"));
161
+ return pkg.version ?? "unknown";
162
+ } catch {
163
+ return "unknown";
164
+ }
165
+ }
166
+ function buildResource(cfg) {
167
+ return resourceFromAttributes({
168
+ [ATTR_SERVICE_NAME]: cfg.cliName,
169
+ [ATTR_SERVICE_VERSION]: cfg.cliVersion,
170
+ "telemetry.sdk.name": SDK_NAME,
171
+ "telemetry.sdk.version": sdkVersion(),
172
+ "telemetry.sdk.language": "nodejs",
173
+ ...cfg.resourceAttributes
174
+ });
175
+ }
176
+
177
+ // src/errors.ts
178
+ var ArgvioError = class extends Error {
179
+ constructor(message) {
180
+ super(`argvio: ${message}`);
181
+ this.name = "ArgvioError";
182
+ }
183
+ };
184
+ var errNoProviderResolved = new ArgvioError("no consent provider resolved a tier");
185
+ var errNoConsentFile = new ArgvioError("no consent config file path resolved");
186
+ var errUnknownTierValue = new ArgvioError("unknown tier value in consent config");
187
+
188
+ // src/consentEnv.ts
189
+ var CI_ENV_VARS = [
190
+ "CI",
191
+ "CONTINUOUS_INTEGRATION",
192
+ "GITHUB_ACTIONS",
193
+ "GITLAB_CI",
194
+ "CIRCLECI",
195
+ "TRAVIS",
196
+ "JENKINS_URL",
197
+ "BUILDKITE",
198
+ "TEAMCITY_VERSION",
199
+ "APPVEYOR",
200
+ "TF_BUILD",
201
+ // Azure Pipelines
202
+ "DRONE",
203
+ // Drone CI
204
+ "CODEBUILD_BUILD_ID",
205
+ "BITBUCKET_BUILD_NUMBER"
206
+ ];
207
+ function isCI() {
208
+ return CI_ENV_VARS.some((name) => {
209
+ const value = process.env[name];
210
+ return value !== void 0 && value !== "" && value !== "false" && value !== "0";
211
+ });
212
+ }
213
+ function isDoNotTrackRequested() {
214
+ const value = process.env["DO_NOT_TRACK"];
215
+ if (value === void 0) return false;
216
+ return value !== "" && value !== "0" && value !== "false";
217
+ }
218
+ var EnvConsentProvider = class {
219
+ resolve() {
220
+ if (isCI()) return Tier.Anonymous;
221
+ throw errNoProviderResolved;
222
+ }
223
+ };
224
+ var AttrCommandPath = "cli.command.path";
225
+ var AttrCommandFlags = "cli.command.flags";
226
+ var AttrExitCode = "cli.command.exit_code";
227
+ var AttrHelpUsed = "cli.command.help_used";
228
+ var AttrSessionId = "cli.session.id";
229
+ var AttrErrorCategory = "cli.error.category";
230
+ var AttrErrorMessage = "cli.error.message";
231
+ var AttrErrorStack = "cli.error.stack";
232
+ var MetricCommandDuration = "cli.command.duration";
233
+ function newSessionId() {
234
+ try {
235
+ return randomBytes(16).toString("hex");
236
+ } catch {
237
+ return "";
238
+ }
239
+ }
240
+
241
+ // src/safety.ts
242
+ var ARGVIO_DEBUG = process.env["ARGVIO_DEBUG"] === "1";
243
+ var DroppedCounters = class {
244
+ queueOverflow = 0;
245
+ belowTierCeil = 0;
246
+ recoveredErrors = 0;
247
+ snapshot() {
248
+ return {
249
+ queueOverflowDropped: this.queueOverflow,
250
+ belowTierDropped: this.belowTierCeil,
251
+ recoveredErrors: this.recoveredErrors
252
+ };
253
+ }
254
+ };
255
+ function logRecovered(err) {
256
+ if (!ARGVIO_DEBUG) return;
257
+ const msg = err instanceof Error ? err.message : "(non-error thrown value)";
258
+ process.stderr.write(`argvio: recovered internal error: ${msg}
259
+ `);
260
+ }
261
+ function safeRun(fn, counters) {
262
+ try {
263
+ fn();
264
+ } catch (err) {
265
+ if (counters) counters.recoveredErrors++;
266
+ logRecovered(err);
267
+ }
268
+ }
269
+
270
+ // src/client.ts
271
+ var INSTRUMENTATION_NAME = "@getargvio/argvio";
272
+ var JobQueue = class {
273
+ constructor(maxSize, counters) {
274
+ this.maxSize = maxSize;
275
+ this.counters = counters;
276
+ }
277
+ maxSize;
278
+ counters;
279
+ items = [];
280
+ draining = false;
281
+ push(job) {
282
+ if (this.items.length >= this.maxSize) {
283
+ this.counters.queueOverflow++;
284
+ return;
285
+ }
286
+ this.items.push(job);
287
+ this.scheduleDrain();
288
+ }
289
+ /**
290
+ * Enqueues a control signal (e.g. a drain sentinel) bypassing the size
291
+ * bound, so `waitForDrain` waits for genuine backlog instead of
292
+ * silently racing ahead when the queue happens to be full.
293
+ */
294
+ pushControl(job) {
295
+ this.items.push(job);
296
+ this.scheduleDrain();
297
+ }
298
+ scheduleDrain() {
299
+ if (this.draining) return;
300
+ this.draining = true;
301
+ setImmediate(() => {
302
+ void this.drain();
303
+ });
304
+ }
305
+ async drain() {
306
+ for (; ; ) {
307
+ const job = this.items.shift();
308
+ if (!job) break;
309
+ try {
310
+ await job();
311
+ } catch (err) {
312
+ this.counters.recoveredErrors++;
313
+ if (ARGVIO_DEBUG) {
314
+ const msg = err instanceof Error ? err.message : "(non-error thrown value)";
315
+ process.stderr.write(`argvio: recovered internal error: ${msg}
316
+ `);
317
+ }
318
+ }
319
+ }
320
+ this.draining = false;
321
+ }
322
+ /** Resolves once every job enqueued before this call has finished executing, or `timeoutMs` elapses. */
323
+ async waitForDrain(timeoutMs) {
324
+ const done = new Promise((resolve) => {
325
+ this.pushControl(() => {
326
+ resolve();
327
+ });
328
+ });
329
+ await withTimeout(done, timeoutMs);
330
+ }
331
+ };
332
+ async function withTimeout(promise, timeoutMs) {
333
+ let timer;
334
+ const timeout = new Promise((resolve) => {
335
+ timer = setTimeout(resolve, timeoutMs);
336
+ });
337
+ try {
338
+ await Promise.race([promise, timeout]);
339
+ } finally {
340
+ if (timer) clearTimeout(timer);
341
+ }
342
+ }
343
+ function toError(value) {
344
+ return value instanceof Error ? value : new Error(String(value));
345
+ }
346
+ var Client = class {
347
+ tier;
348
+ disabled;
349
+ /** Set if construction encountered a problem; the Client still degrades to a safe disabled no-op. */
350
+ initError;
351
+ counters = new DroppedCounters();
352
+ queue;
353
+ shutdownTimeoutMs;
354
+ shuttingDown = false;
355
+ shutdownStarted = false;
356
+ sessionId;
357
+ tracerProvider;
358
+ meterProvider;
359
+ loggerProvider;
360
+ logger;
361
+ latencyHist;
362
+ invocationsCounter;
363
+ sessionsCounter;
364
+ errorEventsCounter;
365
+ /** @internal use {@link createClient} */
366
+ constructor(params) {
367
+ this.disabled = params.disabled;
368
+ this.initError = params.initError;
369
+ this.tier = params.tier;
370
+ this.shutdownTimeoutMs = params.shutdownTimeoutMs;
371
+ this.queue = new JobQueue(params.queueSize, this.counters);
372
+ this.tracerProvider = params.tracerProvider;
373
+ this.meterProvider = params.meterProvider;
374
+ this.loggerProvider = params.loggerProvider;
375
+ this.logger = params.logger;
376
+ this.latencyHist = params.latencyHist;
377
+ this.invocationsCounter = params.invocationsCounter;
378
+ this.sessionsCounter = params.sessionsCounter;
379
+ this.errorEventsCounter = params.errorEventsCounter;
380
+ }
381
+ enabled() {
382
+ return !this.disabled && !this.shuttingDown;
383
+ }
384
+ /** A snapshot of in-process counters about dropped records. Never sent over the network. */
385
+ stats() {
386
+ return this.counters.snapshot();
387
+ }
388
+ gate(required) {
389
+ if (!this.enabled()) return false;
390
+ if (!tierAllows(this.tier, required)) {
391
+ this.counters.belowTierCeil++;
392
+ return false;
393
+ }
394
+ return true;
395
+ }
396
+ tierAttr() {
397
+ return { [RESOURCE_ATTR_TIER]: tierToString(this.tier) };
398
+ }
399
+ emitEvent(name, severity, attributes = {}) {
400
+ this.queue.push(() => {
401
+ this.logger?.emit({
402
+ eventName: name,
403
+ timestamp: Date.now(),
404
+ severityNumber: severity,
405
+ attributes: { ...this.tierAttr(), ...attributes }
406
+ });
407
+ });
408
+ }
409
+ /**
410
+ * Lazily generates a session correlation ID at `Tier.Full`+. A random,
411
+ * non-persistent identifier scoped to a single process invocation —
412
+ * not a user or device identifier.
413
+ */
414
+ ensureSessionId() {
415
+ if (!tierAllows(this.tier, Tier.Full)) return "";
416
+ if (this.sessionId === void 0) {
417
+ this.sessionId = newSessionId();
418
+ }
419
+ return this.sessionId;
420
+ }
421
+ /**
422
+ * Records the start of a CLI process session. Call once, as early as
423
+ * possible. At `Tier.Anonymous` this only increments an aggregate
424
+ * session counter. At `Tier.Basic`+ it also emits a `cli.session_start`
425
+ * event. At `Tier.Full`+ the event carries a per-process session
426
+ * correlation ID.
427
+ */
428
+ recordSessionStart() {
429
+ safeRun(() => {
430
+ if (!this.enabled()) return;
431
+ this.queue.push(() => {
432
+ this.sessionsCounter?.add(1, this.tierAttr());
433
+ });
434
+ if (!tierAllows(this.tier, Tier.Basic)) return;
435
+ const attrs = {};
436
+ const id = this.ensureSessionId();
437
+ if (id) attrs[AttrSessionId] = id;
438
+ this.emitEvent("cli.session_start", SeverityNumber.INFO, attrs);
439
+ }, this.counters);
440
+ }
441
+ /** Records the end of a CLI process session. Call once, symmetric with {@link recordSessionStart}. */
442
+ recordSessionEnd() {
443
+ safeRun(() => {
444
+ if (!this.enabled() || !tierAllows(this.tier, Tier.Basic)) return;
445
+ const attrs = {};
446
+ const id = this.ensureSessionId();
447
+ if (id) attrs[AttrSessionId] = id;
448
+ this.emitEvent("cli.session_end", SeverityNumber.INFO, attrs);
449
+ }, this.counters);
450
+ }
451
+ /**
452
+ * Records that a command began executing. `commandPath` should be the
453
+ * full command path (e.g. `"mycli sub subsub"`), not just the leaf
454
+ * command name. `flagNames` is the list of flag *names* the user set —
455
+ * never flag values, which this SDK never accepts anywhere in the
456
+ * taxonomy.
457
+ *
458
+ * At `Tier.Anonymous` this only increments an aggregate invocation
459
+ * counter with no path/flag detail. At `Tier.Basic`+ the full command
460
+ * path and flag names are included.
461
+ */
462
+ recordCommandInvocation(commandPath, flagNames = []) {
463
+ safeRun(() => {
464
+ if (!this.enabled()) return;
465
+ this.queue.push(() => {
466
+ this.invocationsCounter?.add(1, this.tierAttr());
467
+ });
468
+ if (!tierAllows(this.tier, Tier.Basic)) return;
469
+ const attrs = { [AttrCommandPath]: commandPath };
470
+ if (flagNames.length > 0) attrs[AttrCommandFlags] = [...flagNames];
471
+ this.emitEvent("cli.command_invocation", SeverityNumber.INFO, attrs);
472
+ }, this.counters);
473
+ }
474
+ /**
475
+ * Records the exit code a command finished with. Pass `""` for
476
+ * `commandPath` if unknown (e.g. a single-command CLI). At
477
+ * `Tier.Anonymous` the exit code is recorded without a command path.
478
+ * At `Tier.Basic`+ the command path is included.
479
+ */
480
+ recordExitCode(commandPath, code) {
481
+ safeRun(() => {
482
+ if (!this.gate(Tier.Anonymous)) return;
483
+ const attrs = { [AttrExitCode]: code };
484
+ if (tierAllows(this.tier, Tier.Basic) && commandPath) {
485
+ attrs[AttrCommandPath] = commandPath;
486
+ }
487
+ const severity = code !== 0 ? SeverityNumber.WARN : SeverityNumber.INFO;
488
+ this.emitEvent("cli.exit_code", severity, attrs);
489
+ }, this.counters);
490
+ }
491
+ /**
492
+ * Records how long a command took to execute, in milliseconds, on a
493
+ * histogram (`cli.command.duration`). Available at every tier.
494
+ */
495
+ recordLatency(commandPath, durationMs) {
496
+ safeRun(() => {
497
+ if (!this.gate(Tier.Anonymous)) return;
498
+ const attrs = { ...this.tierAttr() };
499
+ if (tierAllows(this.tier, Tier.Basic) && commandPath) {
500
+ attrs[AttrCommandPath] = commandPath;
501
+ }
502
+ this.queue.push(() => {
503
+ this.latencyHist?.record(durationMs, attrs);
504
+ });
505
+ }, this.counters);
506
+ }
507
+ /** Records that a command's help output was requested (`-h`/`--help` or an explicit help command). */
508
+ recordHelpFlagUsage(commandPath) {
509
+ safeRun(() => {
510
+ if (!this.gate(Tier.Anonymous)) return;
511
+ const attrs = { [AttrHelpUsed]: true };
512
+ if (tierAllows(this.tier, Tier.Basic) && commandPath) {
513
+ attrs[AttrCommandPath] = commandPath;
514
+ }
515
+ this.emitEvent("cli.help_flag_used", SeverityNumber.INFO, attrs);
516
+ }, this.counters);
517
+ }
518
+ /**
519
+ * Records that a command failed with an error, classified by
520
+ * `category` rather than its raw message or stack trace — this is
521
+ * deliberately the only error-recording capability available below
522
+ * `Tier.OptIn`. Raw error detail is only reachable via {@link optIn}.
523
+ *
524
+ * Requires `Tier.Full`+; at lower tiers this is a no-op (counted in
525
+ * `stats().belowTierDropped`).
526
+ */
527
+ recordError(commandPath, category = ErrorCategory.Unknown) {
528
+ safeRun(() => {
529
+ if (!this.gate(Tier.Full)) return;
530
+ const cat = isValidErrorCategory(category) ? category : ErrorCategory.Unknown;
531
+ const attrs = { [AttrErrorCategory]: cat };
532
+ if (commandPath) attrs[AttrCommandPath] = commandPath;
533
+ this.queue.push(() => {
534
+ this.errorEventsCounter?.add(1, { ...this.tierAttr(), [AttrErrorCategory]: cat });
535
+ });
536
+ this.emitEvent("cli.error", SeverityNumber.ERROR, attrs);
537
+ }, this.counters);
538
+ }
539
+ /**
540
+ * Returns an {@link OptInScope} if, and only if, this Client's resolved
541
+ * consent tier is `Tier.OptIn`; otherwise returns `undefined`. There is
542
+ * no other way to reach raw error detail (message, stack trace)
543
+ * anywhere in this package — it's structurally impossible to pass that
544
+ * data through {@link recordError}'s signature.
545
+ */
546
+ optIn() {
547
+ if (this.enabled() && this.tier === Tier.OptIn) return new OptInScope(this);
548
+ return void 0;
549
+ }
550
+ /** @internal called only by {@link OptInScope}. */
551
+ _recordErrorDetail(commandPath, err, stack) {
552
+ safeRun(() => {
553
+ if (!this.gate(Tier.OptIn)) return;
554
+ const attrs = { [AttrErrorMessage]: err.message };
555
+ if (commandPath) attrs[AttrCommandPath] = commandPath;
556
+ if (stack) attrs[AttrErrorStack] = stack;
557
+ this.emitEvent("cli.error_detail", SeverityNumber.ERROR, attrs);
558
+ }, this.counters);
559
+ }
560
+ /**
561
+ * Blocks until all telemetry queued so far has been handed to the
562
+ * exporters and an export attempt has completed, or `timeoutMs`
563
+ * elapses, whichever comes first. Safe to call on a disabled Client
564
+ * (a no-op).
565
+ */
566
+ async flush(timeoutMs = this.shutdownTimeoutMs) {
567
+ if (!this.enabled()) return;
568
+ await this.queue.waitForDrain(timeoutMs);
569
+ await withTimeout(
570
+ Promise.all(
571
+ [
572
+ this.tracerProvider?.forceFlush(),
573
+ this.meterProvider?.forceFlush(),
574
+ this.loggerProvider?.forceFlush()
575
+ ].filter((p) => Boolean(p))
576
+ ).then(() => void 0),
577
+ timeoutMs
578
+ );
579
+ }
580
+ /**
581
+ * Flushes any queued telemetry (best-effort, bounded by `timeoutMs`)
582
+ * and releases all background resources. After `shutdown`, the Client
583
+ * permanently behaves as a disabled no-op. Safe to call on a disabled
584
+ * Client, and safe to call more than once.
585
+ */
586
+ async shutdown(timeoutMs = this.shutdownTimeoutMs) {
587
+ if (this.disabled || this.shutdownStarted) return;
588
+ this.shutdownStarted = true;
589
+ this.shuttingDown = true;
590
+ await this.queue.waitForDrain(timeoutMs);
591
+ await withTimeout(
592
+ Promise.all(
593
+ [
594
+ this.tracerProvider?.shutdown(),
595
+ this.meterProvider?.shutdown(),
596
+ this.loggerProvider?.shutdown()
597
+ ].filter((p) => Boolean(p))
598
+ ).then(() => void 0),
599
+ timeoutMs
600
+ );
601
+ }
602
+ };
603
+ var OptInScope = class {
604
+ constructor(client) {
605
+ this.client = client;
606
+ }
607
+ client;
608
+ /** Records an error's message at full fidelity. */
609
+ recordErrorDetail(commandPath, err) {
610
+ this.client._recordErrorDetail(commandPath, err);
611
+ }
612
+ /** Identical to {@link recordErrorDetail} but also attaches a raw stack trace. */
613
+ recordErrorDetailWithStack(commandPath, err, stack) {
614
+ this.client._recordErrorDetail(commandPath, err, stack);
615
+ }
616
+ };
617
+ function resolveConsentTier(cfg) {
618
+ if (!cfg.consentProvider) return cfg.defaultTier;
619
+ try {
620
+ const tier = cfg.consentProvider.resolve();
621
+ return isValidTier(tier) ? tier : cfg.defaultTier;
622
+ } catch {
623
+ return cfg.defaultTier;
624
+ }
625
+ }
626
+ function disabledClient(initError = null) {
627
+ return new Client({
628
+ disabled: true,
629
+ initError,
630
+ tier: Tier.Anonymous,
631
+ queueSize: 0,
632
+ shutdownTimeoutMs: 0
633
+ });
634
+ }
635
+ function createClient(apiKey, cliName, cliVersion, options = {}) {
636
+ try {
637
+ const cfg = resolveConfig(apiKey, cliName, cliVersion, options);
638
+ if (cfg.disabled || process.env["ARGVIO_DISABLED"] === "1" || isDoNotTrackRequested()) {
639
+ return disabledClient();
640
+ }
641
+ const tier = resolveConsentTier(cfg);
642
+ const exporters = buildExporters(cfg);
643
+ const resource = buildResource(cfg);
644
+ const tracerProvider = new NodeTracerProvider({
645
+ resource,
646
+ spanProcessors: [
647
+ new BatchSpanProcessor(exporters.trace, {
648
+ maxQueueSize: cfg.queueSize,
649
+ exportTimeoutMillis: cfg.exportTimeoutMs
650
+ })
651
+ ]
652
+ });
653
+ const meterProvider = new MeterProvider({
654
+ resource,
655
+ readers: [
656
+ new PeriodicExportingMetricReader({
657
+ exporter: exporters.metric,
658
+ exportTimeoutMillis: cfg.exportTimeoutMs
659
+ })
660
+ ]
661
+ });
662
+ const loggerProvider = new LoggerProvider({
663
+ resource,
664
+ processors: [
665
+ new BatchLogRecordProcessor({
666
+ exporter: exporters.log,
667
+ maxQueueSize: cfg.queueSize,
668
+ exportTimeoutMillis: cfg.exportTimeoutMs
669
+ })
670
+ ]
671
+ });
672
+ const meter = meterProvider.getMeter(INSTRUMENTATION_NAME);
673
+ const logger = loggerProvider.getLogger(INSTRUMENTATION_NAME);
674
+ return new Client({
675
+ disabled: false,
676
+ initError: null,
677
+ tier,
678
+ queueSize: cfg.queueSize,
679
+ shutdownTimeoutMs: cfg.shutdownTimeoutMs,
680
+ tracerProvider,
681
+ meterProvider,
682
+ loggerProvider,
683
+ logger,
684
+ latencyHist: meter.createHistogram(MetricCommandDuration, {
685
+ unit: "ms",
686
+ description: "Duration of a CLI command invocation."
687
+ }),
688
+ invocationsCounter: meter.createCounter("cli.invocations", {
689
+ description: "Count of CLI command invocations."
690
+ }),
691
+ sessionsCounter: meter.createCounter("cli.sessions", {
692
+ description: "Count of CLI process sessions."
693
+ }),
694
+ errorEventsCounter: meter.createCounter("cli.errors", {
695
+ description: "Count of recorded CLI errors."
696
+ })
697
+ });
698
+ } catch (err) {
699
+ return disabledClient(toError(err));
700
+ }
701
+ }
702
+
703
+ // src/consent.ts
704
+ var StaticProvider = class {
705
+ constructor(tier) {
706
+ this.tier = tier;
707
+ }
708
+ tier;
709
+ resolve() {
710
+ return isValidTier(this.tier) ? this.tier : Tier.Anonymous;
711
+ }
712
+ };
713
+ var DisabledProvider = class {
714
+ resolve() {
715
+ return Tier.Anonymous;
716
+ }
717
+ };
718
+ function chainProviders(...providers) {
719
+ return {
720
+ resolve() {
721
+ for (const provider of providers) {
722
+ if (!provider) continue;
723
+ try {
724
+ return provider.resolve();
725
+ } catch {
726
+ continue;
727
+ }
728
+ }
729
+ throw errNoProviderResolved;
730
+ }
731
+ };
732
+ }
733
+ function userConfigDir() {
734
+ switch (process.platform) {
735
+ case "win32":
736
+ return process.env["APPDATA"];
737
+ case "darwin":
738
+ return join(homedir(), "Library", "Application Support");
739
+ default:
740
+ return process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config");
741
+ }
742
+ }
743
+ function defaultConsentPath(cliName) {
744
+ const dir = userConfigDir();
745
+ if (!dir) return void 0;
746
+ return join(dir, cliName || "argvio", "argvio-consent.json");
747
+ }
748
+ var FileConsentProvider = class {
749
+ /** The resolved config file path (after defaults/overrides), primarily for diagnostics and tests. */
750
+ path;
751
+ constructor(cliName, options = {}) {
752
+ this.path = options.path || defaultConsentPath(cliName);
753
+ }
754
+ /**
755
+ * A missing file, unreadable file, or malformed contents all throw
756
+ * synchronously, so callers should place `FileConsentProvider` inside
757
+ * `chainProviders` with a sane fallback (e.g.
758
+ * `new StaticProvider(Tier.Anonymous)`).
759
+ */
760
+ resolve() {
761
+ if (!this.path) throw errNoConsentFile;
762
+ const data = readFileSync(this.path, "utf8");
763
+ const parsed = JSON.parse(data);
764
+ const tier = parseTier(parsed.tier ?? "");
765
+ if (tier === void 0) throw errUnknownTierValue;
766
+ return tier;
767
+ }
768
+ /**
769
+ * Persists `tier` as the chosen consent tier, creating the parent
770
+ * directory if needed. Intended for use by a vendor's first-run
771
+ * consent prompt. Does not itself enforce any tier ceiling — it's the
772
+ * vendor's own UX writing the user's explicit choice.
773
+ */
774
+ store(tier) {
775
+ if (!this.path) throw errNoConsentFile;
776
+ if (!isValidTier(tier)) throw errUnknownTierValue;
777
+ mkdirSync(dirname(this.path), { recursive: true, mode: 448 });
778
+ const contents = { tier: tierToString(tier) };
779
+ writeFileSync(this.path, JSON.stringify(contents), { mode: 384 });
780
+ }
781
+ };
782
+
783
+ export { AttrCommandFlags, AttrCommandPath, AttrErrorCategory, AttrErrorMessage, AttrErrorStack, AttrExitCode, AttrHelpUsed, AttrSessionId, Client, DEFAULT_AUTH_HEADER, DEFAULT_ENDPOINT_GRPC, DEFAULT_ENDPOINT_HTTP, DEFAULT_EXPORT_TIMEOUT_MS, DEFAULT_QUEUE_SIZE, DEFAULT_SHUTDOWN_TIMEOUT_MS, DisabledProvider, EnvConsentProvider, ErrorCategory, FileConsentProvider, MetricCommandDuration, OptInScope, RESOURCE_ATTR_TIER, StaticProvider, Tier, chainProviders, clampTier, createClient, isCI, isDoNotTrackRequested, isValidErrorCategory, isValidTier, parseTier, tierAllows, tierToString };
784
+ //# sourceMappingURL=index.js.map
785
+ //# sourceMappingURL=index.js.map