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