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