@getargvio/argvio 0.1.0 → 0.5.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 CHANGED
@@ -1,7 +1,11 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api';
1
2
  import { SeverityNumber } from '@opentelemetry/api-logs';
2
3
  import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
3
4
  import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
4
5
  import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
6
+ import { homedir } from 'os';
7
+ import { dirname, join } from 'path';
8
+ import { readFileSync, mkdirSync, writeFileSync, statSync } from 'fs';
5
9
  import { Metadata } from '@grpc/grpc-js';
6
10
  import { OTLPTraceExporter as OTLPTraceExporter$1 } from '@opentelemetry/exporter-trace-otlp-http';
7
11
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
@@ -9,22 +13,22 @@ import { OTLPMetricExporter as OTLPMetricExporter$1 } from '@opentelemetry/expor
9
13
  import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
10
14
  import { OTLPLogExporter as OTLPLogExporter$1 } from '@opentelemetry/exporter-logs-otlp-http';
11
15
  import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
12
- import { readFileSync, mkdirSync, writeFileSync } from 'fs';
13
16
  import { resourceFromAttributes } from '@opentelemetry/resources';
14
17
  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
+ import { randomBytes, randomUUID } from 'crypto';
18
19
 
19
20
  // src/client.ts
20
21
 
21
22
  // src/tier.ts
22
23
  var Tier = {
23
- /** No persistent or cross-invocation identifiers; aggregate counts only. */
24
+ /**
25
+ * No persistent or cross-invocation identifiers: command name, exit
26
+ * code, duration, CLI version, OS/arch and CI environment only.
27
+ */
24
28
  Anonymous: 0,
25
- /** Adds command path, flag names (not values), exit code, and latency. */
29
+ /** Adds an opaque per-process session ID, an opaque per-install ID, flag names (never values), and classified (not raw) error types. */
26
30
  Basic: 1,
27
- /** Adds session correlation and classified (not raw) error signatures. */
31
+ /** Reserved: the server allowlist permits some fields here, but this SDK sends nothing beyond `Tier.Basic`. */
28
32
  Full: 2,
29
33
  /**
30
34
  * The explicit opt-in tier: raw error detail (stack traces, messages)
@@ -38,7 +42,7 @@ var TIER_STRINGS = {
38
42
  [Tier.Anonymous]: "anonymous",
39
43
  [Tier.Basic]: "basic",
40
44
  [Tier.Full]: "full",
41
- [Tier.OptIn]: "opt-in"
45
+ [Tier.OptIn]: "optin_plus"
42
46
  };
43
47
  function tierToString(tier) {
44
48
  return TIER_STRINGS[tier] ?? "unknown";
@@ -51,6 +55,7 @@ function parseTier(value) {
51
55
  return Tier.Basic;
52
56
  case "full":
53
57
  return Tier.Full;
58
+ case "optin_plus":
54
59
  case "opt-in":
55
60
  return Tier.OptIn;
56
61
  default:
@@ -82,6 +87,72 @@ function isValidErrorCategory(value) {
82
87
  return ERROR_CATEGORY_VALUES.has(value);
83
88
  }
84
89
 
90
+ // src/errors.ts
91
+ var ArgvioError = class extends Error {
92
+ constructor(message) {
93
+ super(`argvio: ${message}`);
94
+ this.name = "ArgvioError";
95
+ }
96
+ };
97
+ var errNoProviderResolved = new ArgvioError("no consent provider resolved a tier");
98
+ var errNoConsentFile = new ArgvioError("no consent config file path resolved");
99
+ var errUnknownTierValue = new ArgvioError("unknown tier value in consent config");
100
+
101
+ // src/consentFile.ts
102
+ function userConfigDir() {
103
+ switch (process.platform) {
104
+ case "win32":
105
+ return process.env["APPDATA"];
106
+ case "darwin":
107
+ return join(homedir(), "Library", "Application Support");
108
+ default:
109
+ return process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config");
110
+ }
111
+ }
112
+ function defaultInstallIdPath(cliName) {
113
+ const consentPath = defaultConsentPath(cliName);
114
+ return consentPath ? join(dirname(consentPath), "argvio-install-id") : void 0;
115
+ }
116
+ function defaultConsentPath(cliName) {
117
+ const dir = userConfigDir();
118
+ if (!dir) return void 0;
119
+ return join(dir, cliName || "argvio", "argvio-consent.json");
120
+ }
121
+ var FileConsentProvider = class {
122
+ /** The resolved config file path (after defaults/overrides), primarily for diagnostics and tests. */
123
+ path;
124
+ constructor(cliName, options = {}) {
125
+ this.path = options.path || defaultConsentPath(cliName);
126
+ }
127
+ /**
128
+ * A missing file, unreadable file, or malformed contents all throw
129
+ * synchronously, so callers should place `FileConsentProvider` inside
130
+ * `chainProviders` with a sane fallback (e.g.
131
+ * `new StaticProvider(Tier.Anonymous)`).
132
+ */
133
+ resolve() {
134
+ if (!this.path) throw errNoConsentFile;
135
+ const data = readFileSync(this.path, "utf8");
136
+ const parsed = JSON.parse(data);
137
+ const tier = parseTier(parsed.tier ?? "");
138
+ if (tier === void 0) throw errUnknownTierValue;
139
+ return tier;
140
+ }
141
+ /**
142
+ * Persists `tier` as the chosen consent tier, creating the parent
143
+ * directory if needed. Intended for use by a vendor's first-run
144
+ * consent prompt. Does not itself enforce any tier ceiling — it's the
145
+ * vendor's own UX writing the user's explicit choice.
146
+ */
147
+ store(tier) {
148
+ if (!this.path) throw errNoConsentFile;
149
+ if (!isValidTier(tier)) throw errUnknownTierValue;
150
+ mkdirSync(dirname(this.path), { recursive: true, mode: 448 });
151
+ const contents = { tier: tierToString(tier) };
152
+ writeFileSync(this.path, JSON.stringify(contents), { mode: 384 });
153
+ }
154
+ };
155
+
85
156
  // src/options.ts
86
157
  var DEFAULT_ENDPOINT_HTTP = "https://ingest.argvio.io:4318";
87
158
  var DEFAULT_ENDPOINT_GRPC = "ingest.argvio.io:4317";
@@ -89,6 +160,7 @@ var DEFAULT_AUTH_HEADER = "x-argvio-api-key";
89
160
  var DEFAULT_EXPORT_TIMEOUT_MS = 1e4;
90
161
  var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
91
162
  var DEFAULT_QUEUE_SIZE = 2048;
163
+ var MAX_INSTALL_METHOD_LENGTH = 32;
92
164
  function resolveConfig(apiKey, cliName, cliVersion, options = {}) {
93
165
  const transport = options.transport ?? "http";
94
166
  return {
@@ -105,7 +177,9 @@ function resolveConfig(apiKey, cliName, cliVersion, options = {}) {
105
177
  exportTimeoutMs: options.exportTimeoutMs && options.exportTimeoutMs > 0 ? options.exportTimeoutMs : DEFAULT_EXPORT_TIMEOUT_MS,
106
178
  shutdownTimeoutMs: options.shutdownTimeoutMs && options.shutdownTimeoutMs > 0 ? options.shutdownTimeoutMs : DEFAULT_SHUTDOWN_TIMEOUT_MS,
107
179
  queueSize: options.queueSize && options.queueSize > 0 ? options.queueSize : DEFAULT_QUEUE_SIZE,
108
- resourceAttributes: options.resourceAttributes ?? {}
180
+ resourceAttributes: options.resourceAttributes ?? {},
181
+ installIdPath: options.installIdPath !== void 0 ? options.installIdPath || void 0 : defaultInstallIdPath(cliName),
182
+ installMethod: options.installMethod?.slice(0, MAX_INSTALL_METHOD_LENGTH) || void 0
109
183
  };
110
184
  }
111
185
  var SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
@@ -153,37 +227,6 @@ function buildExporters(cfg) {
153
227
  })
154
228
  };
155
229
  }
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
230
 
188
231
  // src/consentEnv.ts
189
232
  var CI_ENV_VARS = [
@@ -210,6 +253,21 @@ function isCI() {
210
253
  return value !== void 0 && value !== "" && value !== "false" && value !== "0";
211
254
  });
212
255
  }
256
+ var CI_PROVIDERS = [
257
+ ["GITHUB_ACTIONS", "github_actions"],
258
+ ["GITLAB_CI", "gitlab_ci"],
259
+ ["CIRCLECI", "circleci"],
260
+ ["BUILDKITE", "buildkite"],
261
+ ["JENKINS_URL", "jenkins"],
262
+ ["TRAVIS", "travis"]
263
+ ];
264
+ function detectCIProvider() {
265
+ for (const [envVar, provider] of CI_PROVIDERS) {
266
+ const value = process.env[envVar];
267
+ if (value && value !== "false" && value !== "0") return provider;
268
+ }
269
+ return isCI() ? "unknown" : void 0;
270
+ }
213
271
  function isDoNotTrackRequested() {
214
272
  const value = process.env["DO_NOT_TRACK"];
215
273
  if (value === void 0) return false;
@@ -221,15 +279,139 @@ var EnvConsentProvider = class {
221
279
  throw errNoProviderResolved;
222
280
  }
223
281
  };
224
- var AttrCommandPath = "cli.command.path";
225
- var AttrCommandFlags = "cli.command.flags";
282
+
283
+ // src/resource.ts
284
+ var ResourceAttrCLIVersion = "cli.version";
285
+ var ResourceAttrOS = "cli.os";
286
+ var ResourceAttrArch = "cli.arch";
287
+ var ResourceAttrIsCI = "cli.is_ci";
288
+ var ResourceAttrCIProvider = "cli.ci_provider";
289
+ function normalizeOS(platform) {
290
+ switch (platform) {
291
+ case "linux":
292
+ case "darwin":
293
+ return platform;
294
+ case "win32":
295
+ return "windows";
296
+ default:
297
+ return "other";
298
+ }
299
+ }
300
+ function normalizeArch(arch) {
301
+ switch (arch) {
302
+ case "x64":
303
+ return "amd64";
304
+ case "arm64":
305
+ case "arm":
306
+ return arch;
307
+ case "ia32":
308
+ return "386";
309
+ default:
310
+ return "other";
311
+ }
312
+ }
313
+ var SDK_NAME = "@getargvio/argvio";
314
+ function sdkVersion() {
315
+ try {
316
+ const pkgUrl = new URL("../package.json", import.meta.url);
317
+ const pkg = JSON.parse(readFileSync(pkgUrl, "utf8"));
318
+ return pkg.version ?? "unknown";
319
+ } catch {
320
+ return "unknown";
321
+ }
322
+ }
323
+ function buildResource(cfg) {
324
+ const ciProvider = detectCIProvider();
325
+ return resourceFromAttributes({
326
+ [ATTR_SERVICE_NAME]: cfg.cliName,
327
+ [ATTR_SERVICE_VERSION]: cfg.cliVersion,
328
+ "telemetry.sdk.name": SDK_NAME,
329
+ "telemetry.sdk.version": sdkVersion(),
330
+ "telemetry.sdk.language": "nodejs",
331
+ [ResourceAttrCLIVersion]: cfg.cliVersion,
332
+ [ResourceAttrOS]: normalizeOS(process.platform),
333
+ [ResourceAttrArch]: normalizeArch(process.arch),
334
+ [ResourceAttrIsCI]: ciProvider !== void 0,
335
+ ...ciProvider ? { [ResourceAttrCIProvider]: ciProvider } : {},
336
+ ...cfg.resourceAttributes
337
+ });
338
+ }
339
+ var AttrCommandName = "cli.command.name";
226
340
  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";
341
+ var AttrDurationMs = "cli.command.duration_ms";
342
+ var AttrPeakRssBytes = "cli.process.peak_rss_bytes";
343
+ var AttrCpuTimeMs = "cli.process.cpu_time_ms";
344
+ var AttrBinarySizeBytes = "cli.binary.size_bytes";
345
+ var AttrStartupDurationMs = "cli.startup.duration_ms";
346
+ var AttrFlagNames = "cli.flag.names";
347
+ var AttrSessionId = "cli.session_id";
348
+ var AttrInstallId = "cli.install_id";
349
+ var AttrHelpFlag = "cli.help.flag";
350
+ var AttrInstallMethod = "cli.install.method";
351
+ var AttrActivationFeature = "cli.activation.feature";
352
+ var AttrErrorType = "cli.error.type";
353
+ var AttrErrorReason = "cli.error.reason";
230
354
  var AttrErrorMessage = "cli.error.message";
231
- var AttrErrorStack = "cli.error.stack";
355
+ var AttrErrorStack = "cli.error.stacktrace";
356
+ var AttrCounterName = "cli.counter.name";
357
+ var AttrCounterLabel = "cli.counter.label";
358
+ var EventSessionStarted = "cli.session.started";
359
+ var EventCommandInvoked = "cli.command.invoked";
360
+ var EventCommandCompleted = "cli.command.completed";
361
+ var EventFeatureActivated = "cli.feature.activated";
362
+ var EventHelpViewed = "cli.help.viewed";
363
+ var EventErrorRaised = "cli.error.raised";
364
+ var SpanCommandPrefix = "cli.command.";
365
+ var MetricCommandInvocations = "cli.command.invocations";
232
366
  var MetricCommandDuration = "cli.command.duration";
367
+ var MetricErrors = "cli.errors";
368
+ var MetricCounter = "cli.counter";
369
+ var MAX_COMMAND_NAME_LENGTH = 128;
370
+ var MAX_ACTIVATION_FEATURE_LENGTH = 64;
371
+ var MAX_IDENTIFIER_LENGTH = 64;
372
+ var IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
373
+ function isValidIdentifier(value) {
374
+ return typeof value === "string" && value.length <= MAX_IDENTIFIER_LENGTH && IDENTIFIER_PATTERN.test(value);
375
+ }
376
+ function commandName(cliName, commandPath) {
377
+ let parts = commandPath.split(/\s+/).filter(Boolean);
378
+ if (parts.length > 1 && parts[0] === cliName) parts = parts.slice(1);
379
+ const name = parts.join(".") || cliName || "root";
380
+ return name.slice(0, MAX_COMMAND_NAME_LENGTH);
381
+ }
382
+ function readProcessResourceUsage() {
383
+ const usage = {};
384
+ try {
385
+ const rssBytes = Math.round(process.resourceUsage().maxRSS * 1024);
386
+ if (Number.isFinite(rssBytes) && rssBytes > 0) usage.peakRssBytes = rssBytes;
387
+ } catch {
388
+ }
389
+ try {
390
+ const { user, system } = process.cpuUsage();
391
+ const cpuMs = Math.round((user + system) / 1e3);
392
+ if (Number.isFinite(cpuMs) && cpuMs > 0) usage.cpuTimeMs = cpuMs;
393
+ } catch {
394
+ }
395
+ return usage;
396
+ }
397
+ function readStartupDurationMs() {
398
+ try {
399
+ const ms = Math.round(process.uptime() * 1e3);
400
+ return Number.isFinite(ms) && ms > 0 ? ms : void 0;
401
+ } catch {
402
+ return void 0;
403
+ }
404
+ }
405
+ function readBinarySizeBytes() {
406
+ try {
407
+ const sea = process.getBuiltinModule?.("node:sea");
408
+ if (!sea?.isSea()) return void 0;
409
+ const size = statSync(process.execPath).size;
410
+ return Number.isFinite(size) && size > 0 ? size : void 0;
411
+ } catch {
412
+ return void 0;
413
+ }
414
+ }
233
415
  function newSessionId() {
234
416
  try {
235
417
  return randomBytes(16).toString("hex");
@@ -237,6 +419,24 @@ function newSessionId() {
237
419
  return "";
238
420
  }
239
421
  }
422
+ var INSTALL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
423
+ function loadOrCreateInstallId(path) {
424
+ if (!path) return "";
425
+ try {
426
+ const existing = readFileSync(path, "utf8").trim();
427
+ if (INSTALL_ID_PATTERN.test(existing)) return existing;
428
+ } catch {
429
+ }
430
+ try {
431
+ const id = randomUUID();
432
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
433
+ writeFileSync(path, `${id}
434
+ `, { mode: 384 });
435
+ return id;
436
+ } catch {
437
+ return "";
438
+ }
439
+ }
240
440
 
241
441
  // src/safety.ts
242
442
  var ARGVIO_DEBUG = process.env["ARGVIO_DEBUG"] === "1";
@@ -354,14 +554,21 @@ var Client = class {
354
554
  shuttingDown = false;
355
555
  shutdownStarted = false;
356
556
  sessionId;
557
+ sessionStarted = false;
558
+ cliName;
559
+ installIdPath;
560
+ installMethod;
561
+ installId;
562
+ activatedFeatures = /* @__PURE__ */ new Set();
563
+ tracer;
357
564
  tracerProvider;
358
565
  meterProvider;
359
566
  loggerProvider;
360
567
  logger;
361
568
  latencyHist;
362
569
  invocationsCounter;
363
- sessionsCounter;
364
570
  errorEventsCounter;
571
+ productCounter;
365
572
  /** @internal use {@link createClient} */
366
573
  constructor(params) {
367
574
  this.disabled = params.disabled;
@@ -375,8 +582,12 @@ var Client = class {
375
582
  this.logger = params.logger;
376
583
  this.latencyHist = params.latencyHist;
377
584
  this.invocationsCounter = params.invocationsCounter;
378
- this.sessionsCounter = params.sessionsCounter;
379
585
  this.errorEventsCounter = params.errorEventsCounter;
586
+ this.productCounter = params.productCounter;
587
+ this.cliName = params.cliName ?? "";
588
+ this.installIdPath = params.installIdPath;
589
+ this.installMethod = params.installMethod;
590
+ this.tracer = params.tracer;
380
591
  }
381
592
  enabled() {
382
593
  return !this.disabled && !this.shuttingDown;
@@ -398,109 +609,193 @@ var Client = class {
398
609
  }
399
610
  emitEvent(name, severity, attributes = {}) {
400
611
  this.queue.push(() => {
401
- this.logger?.emit({
402
- eventName: name,
403
- timestamp: Date.now(),
404
- severityNumber: severity,
405
- attributes: { ...this.tierAttr(), ...attributes }
406
- });
612
+ this.emitEventNow(name, severity, attributes);
613
+ });
614
+ }
615
+ emitEventNow(name, severity, attributes = {}) {
616
+ this.logger?.emit({
617
+ eventName: name,
618
+ timestamp: Date.now(),
619
+ severityNumber: severity,
620
+ attributes: { ...this.tierAttr(), ...attributes }
407
621
  });
408
622
  }
623
+ name(commandPath) {
624
+ return commandName(this.cliName, commandPath);
625
+ }
409
626
  /**
410
- * Lazily generates a session correlation ID at `Tier.Full`+. A random,
627
+ * Lazily generates a session correlation ID at `Tier.Basic`+. A random,
411
628
  * non-persistent identifier scoped to a single process invocation —
412
629
  * not a user or device identifier.
413
630
  */
414
631
  ensureSessionId() {
415
- if (!tierAllows(this.tier, Tier.Full)) return "";
632
+ if (!tierAllows(this.tier, Tier.Basic)) return "";
416
633
  if (this.sessionId === void 0) {
417
634
  this.sessionId = newSessionId();
418
635
  }
419
636
  return this.sessionId;
420
637
  }
421
638
  /**
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.
639
+ * The session and install ID attributes for a record, empty below
640
+ * `Tier.Basic`. May read or create the install ID file, so call it from
641
+ * inside a queued job rather than on the caller's stack.
642
+ */
643
+ identityAttrs(sessionId) {
644
+ const attrs = {};
645
+ if (sessionId) attrs[AttrSessionId] = sessionId;
646
+ if (tierAllows(this.tier, Tier.Basic)) {
647
+ this.installId ??= loadOrCreateInstallId(this.installIdPath);
648
+ if (this.installId) attrs[AttrInstallId] = this.installId;
649
+ }
650
+ return attrs;
651
+ }
652
+ /**
653
+ * Records the start of a CLI process session, which is what the server
654
+ * counts sessions, active installs and retention from. Call it as early
655
+ * as possible (the yargs/commander integrations do it for you); only the
656
+ * first call per Client emits anything.
657
+ *
658
+ * At `Tier.Anonymous` the event carries no identifiers. At `Tier.Basic`+
659
+ * it also carries the per-process session ID and the install ID, a
660
+ * random UUID persisted in the CLI's config directory the first time it
661
+ * is needed — never derived from the user or machine.
662
+ *
663
+ * At `Tier.Basic`+ it also carries startup overhead
664
+ * (`cli.startup.duration_ms`): the time from process start to this call,
665
+ * so it covers module loading and framework setup. Call it just before
666
+ * the command runs, as the integrations do. And when running as a Node
667
+ * single executable application, it carries the binary's size
668
+ * (`cli.binary.size_bytes`). Either is omitted if unavailable, never 0.
427
669
  */
428
670
  recordSessionStart() {
429
671
  safeRun(() => {
430
- if (!this.enabled()) return;
672
+ if (!this.enabled() || this.sessionStarted) return;
673
+ this.sessionStarted = true;
674
+ const sessionId = this.ensureSessionId();
675
+ const basic = tierAllows(this.tier, Tier.Basic);
676
+ const startupMs = basic ? readStartupDurationMs() : void 0;
431
677
  this.queue.push(() => {
432
- this.sessionsCounter?.add(1, this.tierAttr());
678
+ const attrs = this.identityAttrs(sessionId);
679
+ if (startupMs !== void 0) attrs[AttrStartupDurationMs] = startupMs;
680
+ if (basic) {
681
+ const sizeBytes = readBinarySizeBytes();
682
+ if (sizeBytes !== void 0) attrs[AttrBinarySizeBytes] = sizeBytes;
683
+ }
684
+ if (this.installMethod) attrs[AttrInstallMethod] = this.installMethod;
685
+ this.emitEventNow(EventSessionStarted, SeverityNumber.INFO, attrs);
433
686
  });
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
687
  }, this.counters);
450
688
  }
451
689
  /**
452
690
  * 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.
691
+ * full command path (e.g. `"mycli sub subsub"`); it is reported as the
692
+ * dotted command name (`"sub.subsub"`). `flagNames` is the list of flag
693
+ * *names* the user set — never flag values, which this SDK never
694
+ * accepts anywhere in the taxonomy.
457
695
  *
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.
696
+ * The command name is recorded at every tier; flag names and the
697
+ * session ID at `Tier.Basic`+.
461
698
  */
462
699
  recordCommandInvocation(commandPath, flagNames = []) {
463
700
  safeRun(() => {
464
701
  if (!this.enabled()) return;
702
+ const attrs = { [AttrCommandName]: this.name(commandPath) };
703
+ if (tierAllows(this.tier, Tier.Basic) && flagNames.length > 0) {
704
+ attrs[AttrFlagNames] = [...flagNames];
705
+ }
706
+ const sessionId = this.ensureSessionId();
465
707
  this.queue.push(() => {
466
- this.invocationsCounter?.add(1, this.tierAttr());
708
+ this.emitEventNow(EventCommandInvoked, SeverityNumber.INFO, {
709
+ ...attrs,
710
+ ...this.identityAttrs(sessionId)
711
+ });
467
712
  });
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
713
  }, this.counters);
473
714
  }
474
715
  /**
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.
716
+ * Records a completed command: a `cli.command.<name>` span covering its
717
+ * execution, plus the `cli.command.invocations` and
718
+ * `cli.command.duration` metrics. This is what the server's command
719
+ * frequency, latency, error-rate and exit-code dashboards are built
720
+ * from, so call it once for every command that ran (the integrations
721
+ * do). Available at every tier; flag names, peak memory and CPU time
722
+ * only at `Tier.Basic`+.
723
+ *
724
+ * Peak memory (`cli.process.peak_rss_bytes`) and CPU time
725
+ * (`cli.process.cpu_time_ms`) are read once from the process, so they are
726
+ * process-wide and cumulative: right for a CLI that runs one command per
727
+ * process, but in a long-lived or multi-command process each call
728
+ * reports totals since the process started. A measure the platform can't
729
+ * provide is omitted.
479
730
  */
480
- recordExitCode(commandPath, code) {
731
+ recordCommand(result) {
481
732
  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;
733
+ if (!this.enabled()) return;
734
+ const durationMs = Math.max(0, result.durationMs);
735
+ const startMs = result.startMs ?? Date.now() - durationMs;
736
+ const name = this.name(result.path);
737
+ const attrs = {
738
+ ...this.tierAttr(),
739
+ [AttrCommandName]: name,
740
+ [AttrExitCode]: result.exitCode
741
+ };
742
+ const spanAttrs = { ...attrs, [AttrDurationMs]: Math.round(durationMs) };
743
+ if (tierAllows(this.tier, Tier.Basic) && result.flagNames && result.flagNames.length > 0) {
744
+ spanAttrs[AttrFlagNames] = [...result.flagNames];
745
+ }
746
+ if (tierAllows(this.tier, Tier.Basic)) {
747
+ const { peakRssBytes, cpuTimeMs } = readProcessResourceUsage();
748
+ if (peakRssBytes !== void 0) spanAttrs[AttrPeakRssBytes] = peakRssBytes;
749
+ if (cpuTimeMs !== void 0) spanAttrs[AttrCpuTimeMs] = cpuTimeMs;
486
750
  }
487
- const severity = code !== 0 ? SeverityNumber.WARN : SeverityNumber.INFO;
488
- this.emitEvent("cli.exit_code", severity, attrs);
751
+ const sessionId = this.ensureSessionId();
752
+ this.queue.push(() => {
753
+ const span = this.tracer?.startSpan(SpanCommandPrefix + name, {
754
+ startTime: startMs,
755
+ attributes: spanAttrs
756
+ });
757
+ if (result.exitCode !== 0) span?.setStatus({ code: SpanStatusCode.ERROR });
758
+ span?.end(startMs + durationMs);
759
+ this.invocationsCounter?.add(1, attrs);
760
+ this.latencyHist?.record(durationMs, attrs);
761
+ this.emitEventNow(EventCommandCompleted, SeverityNumber.INFO, {
762
+ [AttrCommandName]: name,
763
+ [AttrExitCode]: result.exitCode,
764
+ [AttrDurationMs]: Math.round(durationMs),
765
+ ...this.identityAttrs(sessionId)
766
+ });
767
+ });
489
768
  }, this.counters);
490
769
  }
491
770
  /**
492
- * Records how long a command took to execute, in milliseconds, on a
493
- * histogram (`cli.command.duration`). Available at every tier.
771
+ * Records that the user used one of the vendor's features, so the server
772
+ * can tell which features an install has adopted and how soon after
773
+ * installing. Emitted at `Tier.Basic`+ only; nothing is sent at
774
+ * `Tier.Anonymous`. The event carries the feature name, the command name
775
+ * when `commandPath` is given, and the session and install IDs.
776
+ *
777
+ * `feature` must be a fixed identifier chosen by the vendor in code
778
+ * (e.g. `"export-csv"`), **never user input** such as a flag value, path
779
+ * or argument. It is truncated to the server's 64 character limit.
780
+ *
781
+ * Each feature is reported at most once per process, however often this
782
+ * is called, to keep volume down. The server derives "first use" per
783
+ * install itself, so repeat reports within a run add nothing.
494
784
  */
495
- recordLatency(commandPath, durationMs) {
785
+ recordFeatureActivated(feature, commandPath) {
496
786
  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
- }
787
+ if (!this.gate(Tier.Basic)) return;
788
+ const name = feature.slice(0, MAX_ACTIVATION_FEATURE_LENGTH);
789
+ if (!name || this.activatedFeatures.has(name)) return;
790
+ this.activatedFeatures.add(name);
791
+ const attrs = { [AttrActivationFeature]: name };
792
+ if (commandPath) attrs[AttrCommandName] = this.name(commandPath);
793
+ const sessionId = this.ensureSessionId();
502
794
  this.queue.push(() => {
503
- this.latencyHist?.record(durationMs, attrs);
795
+ this.emitEventNow(EventFeatureActivated, SeverityNumber.INFO, {
796
+ ...attrs,
797
+ ...this.identityAttrs(sessionId)
798
+ });
504
799
  });
505
800
  }, this.counters);
506
801
  }
@@ -508,11 +803,10 @@ var Client = class {
508
803
  recordHelpFlagUsage(commandPath) {
509
804
  safeRun(() => {
510
805
  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);
806
+ this.emitEvent(EventHelpViewed, SeverityNumber.INFO, {
807
+ [AttrCommandName]: this.name(commandPath),
808
+ [AttrHelpFlag]: "help"
809
+ });
516
810
  }, this.counters);
517
811
  }
518
812
  /**
@@ -521,19 +815,73 @@ var Client = class {
521
815
  * deliberately the only error-recording capability available below
522
816
  * `Tier.OptIn`. Raw error detail is only reachable via {@link optIn}.
523
817
  *
524
- * Requires `Tier.Full`+; at lower tiers this is a no-op (counted in
525
- * `stats().belowTierDropped`).
818
+ * At every tier this counts the error on the `cli.errors` metric by
819
+ * command. At `Tier.Basic`+ the category is attached as the error type
820
+ * and a `cli.error.raised` event is emitted too, tied to the session and
821
+ * install.
822
+ *
823
+ * `reason` is an optional, finer classification from a fixed vocabulary
824
+ * you define (e.g. `"no-token"`, `"invalid-url"`), sent at `Tier.Basic`+
825
+ * as `cli.error.reason` on both of those. It must be an identifier chosen
826
+ * in code, **never error text or user input**: this SDK does not scrub it.
827
+ * It must match `^[a-z0-9][a-z0-9._-]*$` and be at most 64 bytes; one that
828
+ * doesn't is dropped (the error is still recorded, without a reason).
526
829
  */
527
- recordError(commandPath, category = ErrorCategory.Unknown) {
830
+ recordError(commandPath, category = ErrorCategory.Unknown, reason) {
528
831
  safeRun(() => {
529
- if (!this.gate(Tier.Full)) return;
832
+ if (!this.enabled()) return;
530
833
  const cat = isValidErrorCategory(category) ? category : ErrorCategory.Unknown;
531
- const attrs = { [AttrErrorCategory]: cat };
532
- if (commandPath) attrs[AttrCommandPath] = commandPath;
834
+ const basic = tierAllows(this.tier, Tier.Basic);
835
+ const attrs = { [AttrCommandName]: this.name(commandPath) };
836
+ if (basic) {
837
+ attrs[AttrErrorType] = cat;
838
+ if (isValidIdentifier(reason)) attrs[AttrErrorReason] = reason;
839
+ }
840
+ const sessionId = this.ensureSessionId();
533
841
  this.queue.push(() => {
534
- this.errorEventsCounter?.add(1, { ...this.tierAttr(), [AttrErrorCategory]: cat });
842
+ this.errorEventsCounter?.add(1, { ...this.tierAttr(), ...attrs });
843
+ if (basic) {
844
+ this.emitEventNow(EventErrorRaised, SeverityNumber.ERROR, {
845
+ ...attrs,
846
+ ...this.identityAttrs(sessionId)
847
+ });
848
+ }
535
849
  });
536
- this.emitEvent("cli.error", SeverityNumber.ERROR, attrs);
850
+ }, this.counters);
851
+ }
852
+ /**
853
+ * Adds `n` to a vendor-defined counter, sent as the `cli.counter` metric
854
+ * with the `cli.counter.name` and, if given, `cli.counter.label`
855
+ * attributes. Increments are summed in memory: one data point per name
856
+ * and label is sent at each export, however often this is called, so it
857
+ * is cheap in a hot loop.
858
+ *
859
+ * **Counters are accepted at the anonymous tier.** They are sent at every
860
+ * tier, including `Tier.Anonymous` and in CI, even when the user has made
861
+ * no consent choice. Nothing identifies the user, but `name` and `label`
862
+ * are strings you choose, so a mistake here puts text into anonymous
863
+ * data. They must be fixed identifiers chosen in code, **never user
864
+ * input, file paths, host names, URLs, tokens or anything derived from
865
+ * them**. This SDK does not scrub them.
866
+ *
867
+ * `name` and `label` must match `^[a-z0-9][a-z0-9._-]*$` and be at most
868
+ * 64 bytes. If either does not, nothing is recorded: the increment is
869
+ * dropped, not trimmed or sent without its label. `n` must be a positive
870
+ * integer; anything else is ignored. The server caps the number of
871
+ * distinct names and labels per tenant and folds the rest into `other`.
872
+ *
873
+ * Never throws, and does nothing on a disabled Client.
874
+ */
875
+ recordCounter(name, n, options = {}) {
876
+ safeRun(() => {
877
+ if (!this.gate(Tier.Anonymous)) return;
878
+ if (!isValidIdentifier(name)) return;
879
+ if (typeof n !== "number" || !Number.isSafeInteger(n) || n <= 0) return;
880
+ const label = options?.label;
881
+ if (label !== void 0 && !isValidIdentifier(label)) return;
882
+ const attrs = { ...this.tierAttr(), [AttrCounterName]: name };
883
+ if (label !== void 0) attrs[AttrCounterLabel] = label;
884
+ this.productCounter?.add(n, attrs);
537
885
  }, this.counters);
538
886
  }
539
887
  /**
@@ -551,10 +899,12 @@ var Client = class {
551
899
  _recordErrorDetail(commandPath, err, stack) {
552
900
  safeRun(() => {
553
901
  if (!this.gate(Tier.OptIn)) return;
554
- const attrs = { [AttrErrorMessage]: err.message };
555
- if (commandPath) attrs[AttrCommandPath] = commandPath;
902
+ const attrs = {
903
+ [AttrCommandName]: this.name(commandPath),
904
+ [AttrErrorMessage]: err.message
905
+ };
556
906
  if (stack) attrs[AttrErrorStack] = stack;
557
- this.emitEvent("cli.error_detail", SeverityNumber.ERROR, attrs);
907
+ this.emitEvent(EventErrorRaised, SeverityNumber.ERROR, attrs);
558
908
  }, this.counters);
559
909
  }
560
910
  /**
@@ -567,13 +917,11 @@ var Client = class {
567
917
  if (!this.enabled()) return;
568
918
  await this.queue.waitForDrain(timeoutMs);
569
919
  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),
920
+ Promise.allSettled([
921
+ this.tracerProvider?.forceFlush(),
922
+ this.meterProvider?.forceFlush(),
923
+ this.loggerProvider?.forceFlush()
924
+ ]).then(() => void 0),
577
925
  timeoutMs
578
926
  );
579
927
  }
@@ -589,13 +937,11 @@ var Client = class {
589
937
  this.shuttingDown = true;
590
938
  await this.queue.waitForDrain(timeoutMs);
591
939
  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),
940
+ Promise.allSettled([
941
+ this.tracerProvider?.shutdown(),
942
+ this.meterProvider?.shutdown(),
943
+ this.loggerProvider?.shutdown()
944
+ ]).then(() => void 0),
599
945
  timeoutMs
600
946
  );
601
947
  }
@@ -672,6 +1018,10 @@ function createClient(apiKey, cliName, cliVersion, options = {}) {
672
1018
  const meter = meterProvider.getMeter(INSTRUMENTATION_NAME);
673
1019
  const logger = loggerProvider.getLogger(INSTRUMENTATION_NAME);
674
1020
  return new Client({
1021
+ cliName,
1022
+ installIdPath: cfg.installIdPath,
1023
+ installMethod: cfg.installMethod,
1024
+ tracer: tracerProvider.getTracer(INSTRUMENTATION_NAME),
675
1025
  disabled: false,
676
1026
  initError: null,
677
1027
  tier,
@@ -685,14 +1035,17 @@ function createClient(apiKey, cliName, cliVersion, options = {}) {
685
1035
  unit: "ms",
686
1036
  description: "Duration of a CLI command invocation."
687
1037
  }),
688
- invocationsCounter: meter.createCounter("cli.invocations", {
689
- description: "Count of CLI command invocations."
1038
+ invocationsCounter: meter.createCounter(MetricCommandInvocations, {
1039
+ unit: "1",
1040
+ description: "Count of completed CLI command invocations."
690
1041
  }),
691
- sessionsCounter: meter.createCounter("cli.sessions", {
692
- description: "Count of CLI process sessions."
693
- }),
694
- errorEventsCounter: meter.createCounter("cli.errors", {
1042
+ errorEventsCounter: meter.createCounter(MetricErrors, {
1043
+ unit: "1",
695
1044
  description: "Count of recorded CLI errors."
1045
+ }),
1046
+ productCounter: meter.createCounter(MetricCounter, {
1047
+ unit: "1",
1048
+ description: "Vendor-defined counters, by fixed name and optional label."
696
1049
  })
697
1050
  });
698
1051
  } catch (err) {
@@ -730,56 +1083,7 @@ function chainProviders(...providers) {
730
1083
  }
731
1084
  };
732
1085
  }
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
1086
 
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 };
1087
+ export { AttrActivationFeature, AttrBinarySizeBytes, AttrCommandName, AttrCounterLabel, AttrCounterName, AttrCpuTimeMs, AttrDurationMs, AttrErrorMessage, AttrErrorReason, AttrErrorStack, AttrErrorType, AttrExitCode, AttrFlagNames, AttrHelpFlag, AttrInstallId, AttrInstallMethod, AttrPeakRssBytes, AttrSessionId, AttrStartupDurationMs, Client, DEFAULT_AUTH_HEADER, DEFAULT_ENDPOINT_GRPC, DEFAULT_ENDPOINT_HTTP, DEFAULT_EXPORT_TIMEOUT_MS, DEFAULT_QUEUE_SIZE, DEFAULT_SHUTDOWN_TIMEOUT_MS, DisabledProvider, EnvConsentProvider, ErrorCategory, EventCommandCompleted, EventCommandInvoked, EventErrorRaised, EventFeatureActivated, EventHelpViewed, EventSessionStarted, FileConsentProvider, MetricCommandDuration, MetricCommandInvocations, MetricCounter, MetricErrors, OptInScope, RESOURCE_ATTR_TIER, ResourceAttrArch, ResourceAttrCIProvider, ResourceAttrCLIVersion, ResourceAttrIsCI, ResourceAttrOS, SpanCommandPrefix, StaticProvider, Tier, chainProviders, clampTier, createClient, detectCIProvider, isCI, isDoNotTrackRequested, isValidErrorCategory, isValidTier, parseTier, tierAllows, tierToString };
784
1088
  //# sourceMappingURL=index.js.map
785
1089
  //# sourceMappingURL=index.js.map