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