@opendatalabs/connect 0.12.1 → 0.13.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/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/telemetry-contract.d.ts +146 -0
- package/dist/cli/telemetry-contract.d.ts.map +1 -0
- package/dist/cli/telemetry-contract.js +56 -0
- package/dist/cli/telemetry-contract.js.map +1 -0
- package/dist/cli/telemetry.d.ts +15 -31
- package/dist/cli/telemetry.d.ts.map +1 -1
- package/dist/cli/telemetry.js +390 -213
- package/dist/cli/telemetry.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/telemetry.js
CHANGED
|
@@ -3,13 +3,14 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { getTelemetryOutboxDir, readCliConfig, updateCliConfig, } from "../core/index.js";
|
|
6
|
-
|
|
6
|
+
import { TELEMETRY_ENDPOINT, TELEMETRY_EVENT_VERSION, TELEMETRY_PRODUCER_NAME, } from "./telemetry-contract.js";
|
|
7
|
+
// ── Config ──────────────────────────────────────────────────────────────────
|
|
7
8
|
const MAX_EVENTS_PER_BATCH = 100;
|
|
8
9
|
const MAX_BATCH_BYTES = 64 * 1024;
|
|
9
10
|
const MAX_FILES_PER_FLUSH = 10;
|
|
10
11
|
const FLUSH_TIMEOUT_MS = 1500;
|
|
11
|
-
const EVENT_VERSION = 1;
|
|
12
12
|
let activeSession = null;
|
|
13
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
13
14
|
function randomId(prefix) {
|
|
14
15
|
return `${prefix}_${crypto.randomUUID()}`;
|
|
15
16
|
}
|
|
@@ -19,127 +20,74 @@ function nowIso() {
|
|
|
19
20
|
function getEndpoint() {
|
|
20
21
|
return process.env.VANA_TELEMETRY_URL?.trim() || TELEMETRY_ENDPOINT;
|
|
21
22
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
23
|
+
function detectOs() {
|
|
24
|
+
const platform = process.platform;
|
|
25
|
+
if (platform === "linux")
|
|
26
|
+
return "linux";
|
|
27
|
+
if (platform === "darwin")
|
|
28
|
+
return "macos";
|
|
29
|
+
if (platform === "win32")
|
|
30
|
+
return "windows";
|
|
31
|
+
return "linux"; // fallback
|
|
24
32
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return
|
|
33
|
+
function detectArch() {
|
|
34
|
+
const arch = os.arch();
|
|
35
|
+
if (arch === "arm64")
|
|
36
|
+
return "arm64";
|
|
37
|
+
return "x86_64";
|
|
30
38
|
}
|
|
31
|
-
function
|
|
32
|
-
return {
|
|
33
|
-
storedScopeCount: scopeResults?.filter((item) => item.status === "stored").length ?? null,
|
|
34
|
-
failedScopeCount: scopeResults?.filter((item) => item.status === "failed").length ?? null,
|
|
35
|
-
};
|
|
39
|
+
function makeHostPlatform(osName, arch) {
|
|
40
|
+
return `${osName}-${arch}`;
|
|
36
41
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return "source_required";
|
|
47
|
-
if (value.includes("prompt_cancelled"))
|
|
48
|
-
return "prompt_cancelled";
|
|
49
|
-
if (value.includes("auth"))
|
|
50
|
-
return "auth_failed";
|
|
51
|
-
if (value.includes("needs input"))
|
|
52
|
-
return "needs_input";
|
|
53
|
-
if (value.includes("legacy"))
|
|
54
|
-
return "legacy_auth";
|
|
55
|
-
if (value.includes("personal_server_unavailable"))
|
|
42
|
+
// Classify a free-form CLI error/reason string into a canonical error class.
|
|
43
|
+
// The CLI's own classification produced richer values (e.g. setup_required,
|
|
44
|
+
// legacy_auth); we collapse these into the canonical whitelist here.
|
|
45
|
+
function classifyCanonicalError(value) {
|
|
46
|
+
const normalized = (value ?? "").toLowerCase();
|
|
47
|
+
if (!normalized)
|
|
48
|
+
return "unknown";
|
|
49
|
+
if (normalized.includes("personal_server_unavailable") ||
|
|
50
|
+
normalized.includes("personal server")) {
|
|
56
51
|
return "personal_server_unavailable";
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
52
|
+
}
|
|
53
|
+
if (normalized.includes("auth") || normalized.includes("legacy")) {
|
|
54
|
+
return "auth_failed";
|
|
55
|
+
}
|
|
56
|
+
if (normalized.includes("timeout") || normalized.includes("timed out")) {
|
|
57
|
+
return "timeout";
|
|
58
|
+
}
|
|
59
|
+
if (normalized.includes("network")) {
|
|
60
|
+
return "network_error";
|
|
61
|
+
}
|
|
62
|
+
if (normalized.includes("runtime") ||
|
|
63
|
+
normalized.includes("connector") ||
|
|
64
|
+
normalized.includes("unexpected") ||
|
|
65
|
+
normalized.includes("ingest_failed") ||
|
|
66
|
+
normalized.includes("setup_required") ||
|
|
67
|
+
normalized.includes("invalid_connector")) {
|
|
62
68
|
return "runtime_error";
|
|
69
|
+
}
|
|
63
70
|
return "unknown";
|
|
64
71
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
patch: {
|
|
78
|
-
metadata: sanitizeMetadata({ runtime: event.runtime ?? null }),
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
case "connector-resolved":
|
|
82
|
-
return { eventName: "connector_resolved" };
|
|
83
|
-
case "needs-input":
|
|
84
|
-
return {
|
|
85
|
-
eventName: "input_required",
|
|
86
|
-
patch: {
|
|
87
|
-
errorClass: "needs_input",
|
|
88
|
-
metadata: sanitizeMetadata({ fieldCount: event.fields?.length ?? 0 }),
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
case "legacy-auth":
|
|
92
|
-
return {
|
|
93
|
-
eventName: "legacy_auth_required",
|
|
94
|
-
patch: { errorClass: "legacy_auth" },
|
|
95
|
-
};
|
|
96
|
-
case "collection-complete":
|
|
97
|
-
return { eventName: "collection_completed" };
|
|
98
|
-
case "runtime-error":
|
|
99
|
-
return {
|
|
100
|
-
eventName: "collection_failed",
|
|
101
|
-
patch: { errorClass: classifyError(event.message) ?? "runtime_error" },
|
|
102
|
-
};
|
|
103
|
-
case "ingest-started":
|
|
104
|
-
return { eventName: "ingest_started" };
|
|
105
|
-
case "ingest-complete":
|
|
106
|
-
return {
|
|
107
|
-
eventName: "ingest_completed",
|
|
108
|
-
patch: countScopeResults(event.scopeResults),
|
|
109
|
-
};
|
|
110
|
-
case "ingest-partial":
|
|
111
|
-
return {
|
|
112
|
-
eventName: "ingest_partial",
|
|
113
|
-
patch: {
|
|
114
|
-
...countScopeResults(event.scopeResults),
|
|
115
|
-
errorClass: "ingest_failed",
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
case "ingest-failed":
|
|
119
|
-
return {
|
|
120
|
-
eventName: "ingest_failed",
|
|
121
|
-
patch: {
|
|
122
|
-
...countScopeResults(event.scopeResults),
|
|
123
|
-
errorClass: "ingest_failed",
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
case "ingest-skipped":
|
|
127
|
-
return {
|
|
128
|
-
eventName: "ingest_skipped",
|
|
129
|
-
patch: {
|
|
130
|
-
errorClass: classifyError(event.reason) ?? null,
|
|
131
|
-
metadata: sanitizeMetadata({ reason: event.reason ?? null }),
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
case "outcome":
|
|
135
|
-
case "progress-update":
|
|
136
|
-
case "status-update":
|
|
137
|
-
case "headed-required":
|
|
138
|
-
case "jpeg":
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
72
|
+
// Map a CLI interaction indicator to a canonical InteractionKind.
|
|
73
|
+
function mapInteractionKind(value) {
|
|
74
|
+
const normalized = (value ?? "").toLowerCase();
|
|
75
|
+
if (!normalized)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (normalized.includes("otp"))
|
|
78
|
+
return "otp";
|
|
79
|
+
if (normalized.includes("captcha"))
|
|
80
|
+
return "captcha";
|
|
81
|
+
if (normalized.includes("login") || normalized.includes("credential"))
|
|
82
|
+
return "login";
|
|
83
|
+
return "manual_action";
|
|
142
84
|
}
|
|
85
|
+
// ── Outbox (file-backed) ────────────────────────────────────────────────────
|
|
86
|
+
//
|
|
87
|
+
// Events are batched into TelemetryBatch envelopes, written to individual
|
|
88
|
+
// JSON files in getTelemetryOutboxDir(), and flushed on command exit and on
|
|
89
|
+
// subsequent runs (retry across restarts). This survives crashes — the
|
|
90
|
+
// pattern is worth preserving for the desktop app too.
|
|
143
91
|
async function ensureOutboxDir() {
|
|
144
92
|
await fs.mkdir(getTelemetryOutboxDir(), { recursive: true });
|
|
145
93
|
}
|
|
@@ -222,52 +170,64 @@ async function resolveTelemetryState(localOnly = false) {
|
|
|
222
170
|
queuedBatches,
|
|
223
171
|
};
|
|
224
172
|
}
|
|
173
|
+
// ── Event factory ───────────────────────────────────────────────────────────
|
|
174
|
+
//
|
|
175
|
+
// Builds canonical TelemetryEvent values with identity + time + attribution +
|
|
176
|
+
// context auto-populated. The caller supplies correlation + kind.
|
|
225
177
|
function createEventFactory(context, state) {
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
178
|
+
const hostRunId = randomId("host");
|
|
179
|
+
const osName = detectOs();
|
|
180
|
+
const arch = detectArch();
|
|
181
|
+
const hostPlatform = makeHostPlatform(osName, arch);
|
|
182
|
+
const baseContext = {
|
|
183
|
+
hostPlatform,
|
|
184
|
+
os: osName,
|
|
185
|
+
arch,
|
|
186
|
+
producerVersion: context.cliVersion,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
hostRunId,
|
|
190
|
+
build(args) {
|
|
191
|
+
return {
|
|
192
|
+
identity: {
|
|
193
|
+
eventId: randomId("evt"),
|
|
194
|
+
eventVersion: TELEMETRY_EVENT_VERSION,
|
|
195
|
+
},
|
|
196
|
+
time: {
|
|
197
|
+
occurredAt: nowIso(),
|
|
198
|
+
...(args.durationMs !== undefined
|
|
199
|
+
? { durationMs: args.durationMs }
|
|
200
|
+
: {}),
|
|
201
|
+
},
|
|
202
|
+
attribution: {
|
|
203
|
+
producer: TELEMETRY_PRODUCER_NAME,
|
|
204
|
+
installId: state.installId,
|
|
205
|
+
},
|
|
206
|
+
context: {
|
|
207
|
+
...baseContext,
|
|
208
|
+
...(args.connectorVersion
|
|
209
|
+
? { connectorVersion: args.connectorVersion }
|
|
210
|
+
: {}),
|
|
211
|
+
...(args.authMode ? { authMode: args.authMode } : {}),
|
|
212
|
+
},
|
|
213
|
+
correlation: args.correlation,
|
|
214
|
+
kind: args.kind,
|
|
215
|
+
...(args.debug ? { debug: args.debug } : {}),
|
|
216
|
+
...(args.extensions ? { extensions: args.extensions } : {}),
|
|
217
|
+
};
|
|
218
|
+
},
|
|
244
219
|
};
|
|
245
|
-
return (eventName, patch = {}) => ({
|
|
246
|
-
eventId: randomId("evt"),
|
|
247
|
-
eventVersion: EVENT_VERSION,
|
|
248
|
-
timestamp: nowIso(),
|
|
249
|
-
eventName,
|
|
250
|
-
...base,
|
|
251
|
-
...patch,
|
|
252
|
-
outcome: patch.outcome ?? null,
|
|
253
|
-
errorClass: patch.errorClass ?? null,
|
|
254
|
-
durationMs: patch.durationMs ?? null,
|
|
255
|
-
storedScopeCount: patch.storedScopeCount ?? null,
|
|
256
|
-
failedScopeCount: patch.failedScopeCount ?? null,
|
|
257
|
-
metadata: sanitizeMetadata(patch.metadata),
|
|
258
|
-
});
|
|
259
220
|
}
|
|
260
|
-
|
|
221
|
+
// ── Batch serialization ─────────────────────────────────────────────────────
|
|
222
|
+
function splitIntoEnvelopes(events) {
|
|
261
223
|
const envelopes = [];
|
|
262
224
|
let current = [];
|
|
263
225
|
const flushCurrent = () => {
|
|
264
|
-
if (current.length === 0)
|
|
226
|
+
if (current.length === 0)
|
|
265
227
|
return;
|
|
266
|
-
}
|
|
267
228
|
envelopes.push({
|
|
268
229
|
batchId: randomId("batch"),
|
|
269
230
|
sentAt: nowIso(),
|
|
270
|
-
client: { name: "vana-cli", version: cliVersion },
|
|
271
231
|
events: current,
|
|
272
232
|
});
|
|
273
233
|
current = [];
|
|
@@ -277,7 +237,6 @@ function splitIntoEnvelopes(events, cliVersion) {
|
|
|
277
237
|
const candidate = {
|
|
278
238
|
batchId: "batch_candidate",
|
|
279
239
|
sentAt: nowIso(),
|
|
280
|
-
client: { name: "vana-cli", version: cliVersion },
|
|
281
240
|
events: current,
|
|
282
241
|
};
|
|
283
242
|
const tooManyEvents = current.length > MAX_EVENTS_PER_BATCH;
|
|
@@ -285,25 +244,23 @@ function splitIntoEnvelopes(events, cliVersion) {
|
|
|
285
244
|
if (tooManyEvents || tooLarge) {
|
|
286
245
|
const overflow = current.pop();
|
|
287
246
|
flushCurrent();
|
|
288
|
-
if (overflow)
|
|
247
|
+
if (overflow)
|
|
289
248
|
current.push(overflow);
|
|
290
|
-
}
|
|
291
249
|
}
|
|
292
250
|
}
|
|
293
251
|
flushCurrent();
|
|
294
252
|
return envelopes;
|
|
295
253
|
}
|
|
296
|
-
async function writeEnvelope(envelope,
|
|
254
|
+
async function writeEnvelope(envelope, hostRunId) {
|
|
297
255
|
await ensureOutboxDir();
|
|
298
|
-
const filename = `${Date.now()}-${process.pid}-${
|
|
256
|
+
const filename = `${Date.now()}-${process.pid}-${hostRunId}-${crypto.randomUUID()}.json`;
|
|
299
257
|
const outboxPath = path.join(getTelemetryOutboxDir(), filename);
|
|
300
258
|
await fs.writeFile(outboxPath, `${JSON.stringify(envelope)}\n`, "utf8");
|
|
301
259
|
}
|
|
302
260
|
export async function flushTelemetryOutbox() {
|
|
303
261
|
const state = await resolveTelemetryState();
|
|
304
|
-
if (!state.enabled || state.mode !== "normal")
|
|
262
|
+
if (!state.enabled || state.mode !== "normal")
|
|
305
263
|
return;
|
|
306
|
-
}
|
|
307
264
|
const files = (await listOutboxFiles()).slice(0, MAX_FILES_PER_FLUSH);
|
|
308
265
|
for (const filePath of files) {
|
|
309
266
|
let contents;
|
|
@@ -318,7 +275,7 @@ export async function flushTelemetryOutbox() {
|
|
|
318
275
|
method: "POST",
|
|
319
276
|
headers: {
|
|
320
277
|
"Content-Type": "application/json",
|
|
321
|
-
"User-Agent": `vana-cli
|
|
278
|
+
"User-Agent": `vana-cli/unknown`,
|
|
322
279
|
},
|
|
323
280
|
body: contents,
|
|
324
281
|
signal: AbortSignal.timeout(FLUSH_TIMEOUT_MS),
|
|
@@ -332,6 +289,7 @@ export async function flushTelemetryOutbox() {
|
|
|
332
289
|
}
|
|
333
290
|
}
|
|
334
291
|
}
|
|
292
|
+
// ── Public helpers ──────────────────────────────────────────────────────────
|
|
335
293
|
export async function getTelemetryStatus() {
|
|
336
294
|
const state = await resolveTelemetryState();
|
|
337
295
|
return {
|
|
@@ -352,93 +310,312 @@ export function setActiveTelemetrySession(session) {
|
|
|
352
310
|
export function getActiveTelemetrySession() {
|
|
353
311
|
return activeSession;
|
|
354
312
|
}
|
|
355
|
-
|
|
356
|
-
activeSession?.trackCustomEvent(eventName, patch);
|
|
357
|
-
}
|
|
313
|
+
// ── Session ─────────────────────────────────────────────────────────────────
|
|
358
314
|
export async function createCliTelemetrySession(context) {
|
|
359
315
|
const state = await resolveTelemetryState(Boolean(context.localOnly));
|
|
360
|
-
const
|
|
316
|
+
const factory = createEventFactory(context, state);
|
|
361
317
|
const startedAt = Date.now();
|
|
362
318
|
const events = [];
|
|
363
|
-
|
|
364
|
-
|
|
319
|
+
// Per-session state. A single CLI invocation is ONE host run, with zero
|
|
320
|
+
// or more collection runs and sync attempts nested within.
|
|
321
|
+
const hostRunId = factory.hostRunId;
|
|
322
|
+
let collectionRunId = null;
|
|
323
|
+
let collectionSource = context.source ?? null;
|
|
324
|
+
let connectorVersion;
|
|
325
|
+
let authMode;
|
|
326
|
+
let syncRunId = null;
|
|
327
|
+
let latestOutcomeRaw = null;
|
|
365
328
|
let persisted = false;
|
|
366
|
-
|
|
367
|
-
|
|
329
|
+
// Host-level extensions captured from the command context. These are
|
|
330
|
+
// producer-specific details that don't belong in the canonical Kind.
|
|
331
|
+
const hostExtensions = {
|
|
332
|
+
command: context.command,
|
|
333
|
+
subcommand: context.subcommand ?? null,
|
|
334
|
+
channel: context.channel,
|
|
335
|
+
installMethod: context.installMethod,
|
|
336
|
+
isCi: Boolean(process.env.CI),
|
|
337
|
+
isAgent: Boolean(process.env.AGENT),
|
|
338
|
+
isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY && !context.options.noInput),
|
|
339
|
+
};
|
|
340
|
+
const push = (event) => {
|
|
341
|
+
if (!state.enabled && state.mode !== "debug")
|
|
368
342
|
return;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
343
|
+
events.push(event);
|
|
344
|
+
};
|
|
345
|
+
// Emit host/started immediately.
|
|
346
|
+
push(factory.build({
|
|
347
|
+
correlation: { scope: "host", hostRunId },
|
|
348
|
+
kind: { lifecycle: "host", phase: "started" },
|
|
349
|
+
extensions: hostExtensions,
|
|
350
|
+
}));
|
|
351
|
+
// Helper to lazily start a collection run the first time we see collection activity.
|
|
352
|
+
const ensureCollectionStarted = () => {
|
|
353
|
+
if (collectionRunId)
|
|
354
|
+
return;
|
|
355
|
+
if (!collectionSource)
|
|
356
|
+
return;
|
|
357
|
+
collectionRunId = randomId("col");
|
|
358
|
+
push(factory.build({
|
|
359
|
+
correlation: {
|
|
360
|
+
scope: "collection",
|
|
361
|
+
hostRunId,
|
|
362
|
+
collectionRunId,
|
|
363
|
+
source: collectionSource,
|
|
364
|
+
},
|
|
365
|
+
kind: { lifecycle: "collection", phase: "started" },
|
|
366
|
+
connectorVersion,
|
|
367
|
+
authMode,
|
|
368
|
+
}));
|
|
369
|
+
};
|
|
370
|
+
const emitCollectionEvent = (kind) => {
|
|
371
|
+
ensureCollectionStarted();
|
|
372
|
+
if (!collectionRunId || !collectionSource)
|
|
373
|
+
return;
|
|
374
|
+
push(factory.build({
|
|
375
|
+
correlation: {
|
|
376
|
+
scope: "collection",
|
|
377
|
+
hostRunId,
|
|
378
|
+
collectionRunId,
|
|
379
|
+
source: collectionSource,
|
|
380
|
+
},
|
|
381
|
+
kind,
|
|
382
|
+
connectorVersion,
|
|
383
|
+
}));
|
|
384
|
+
};
|
|
385
|
+
const ensureSyncRunId = () => {
|
|
386
|
+
if (!syncRunId)
|
|
387
|
+
syncRunId = randomId("sync");
|
|
388
|
+
return syncRunId;
|
|
389
|
+
};
|
|
390
|
+
const emitSyncEvent = (kind) => {
|
|
391
|
+
if (!collectionSource)
|
|
392
|
+
return;
|
|
393
|
+
push(factory.build({
|
|
394
|
+
correlation: {
|
|
395
|
+
scope: "sync",
|
|
396
|
+
hostRunId,
|
|
397
|
+
syncRunId: ensureSyncRunId(),
|
|
398
|
+
source: collectionSource,
|
|
399
|
+
...(collectionRunId ? { collectionRunId } : {}),
|
|
400
|
+
},
|
|
401
|
+
kind,
|
|
402
|
+
}));
|
|
403
|
+
};
|
|
404
|
+
const countScopeResults = (scopeResults) => {
|
|
405
|
+
const stored = scopeResults?.filter((s) => s.status === "stored").length ?? 0;
|
|
406
|
+
const failed = scopeResults?.filter((s) => s.status === "failed").length ?? 0;
|
|
407
|
+
return { stored, failed };
|
|
372
408
|
};
|
|
373
|
-
push("command_started", {
|
|
374
|
-
metadata: sanitizeMetadata({
|
|
375
|
-
launchMode: context.options.detach ? "detached" : "direct",
|
|
376
|
-
inputMode: context.options.ipc
|
|
377
|
-
? "ipc"
|
|
378
|
-
: context.options.noInput
|
|
379
|
-
? "no_input"
|
|
380
|
-
: "interactive",
|
|
381
|
-
}),
|
|
382
|
-
});
|
|
383
409
|
return {
|
|
384
410
|
trackCliEvent(event) {
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
411
|
+
// Capture connector version / source / auth mode from any event that carries them.
|
|
412
|
+
if ("source" in event && event.source)
|
|
413
|
+
collectionSource = event.source;
|
|
414
|
+
switch (event.type) {
|
|
415
|
+
case "setup-check":
|
|
416
|
+
// Setup-phase events are producer-specific and don't fit the shared
|
|
417
|
+
// lifecycle. Attach them to the host extensions instead.
|
|
418
|
+
hostExtensions.runtimeCheckCompleted = true;
|
|
419
|
+
break;
|
|
420
|
+
case "setup-complete":
|
|
421
|
+
hostExtensions.runtimeInstallCompleted = true;
|
|
422
|
+
break;
|
|
423
|
+
case "connector-resolved":
|
|
424
|
+
hostExtensions.connectorResolved = true;
|
|
425
|
+
break;
|
|
426
|
+
case "needs-input":
|
|
427
|
+
emitCollectionEvent({
|
|
428
|
+
lifecycle: "collection",
|
|
429
|
+
phase: "needs_input",
|
|
430
|
+
...(mapInteractionKind(event.fields?.join(",") ?? null)
|
|
431
|
+
? {
|
|
432
|
+
interactionKind: mapInteractionKind(event.fields?.join(",") ?? null),
|
|
433
|
+
}
|
|
434
|
+
: {}),
|
|
435
|
+
});
|
|
436
|
+
break;
|
|
437
|
+
case "legacy-auth":
|
|
438
|
+
// Legacy auth is effectively a "needs manual action" interaction.
|
|
439
|
+
emitCollectionEvent({
|
|
440
|
+
lifecycle: "collection",
|
|
441
|
+
phase: "needs_input",
|
|
442
|
+
interactionKind: "manual_action",
|
|
443
|
+
});
|
|
444
|
+
break;
|
|
445
|
+
case "collection-complete":
|
|
446
|
+
emitCollectionEvent({
|
|
447
|
+
lifecycle: "collection",
|
|
448
|
+
phase: "terminal",
|
|
449
|
+
outcome: "success",
|
|
450
|
+
});
|
|
451
|
+
break;
|
|
452
|
+
case "runtime-error":
|
|
453
|
+
emitCollectionEvent({
|
|
454
|
+
lifecycle: "collection",
|
|
455
|
+
phase: "terminal",
|
|
456
|
+
outcome: "failure",
|
|
457
|
+
errorClass: classifyCanonicalError(event.message),
|
|
458
|
+
});
|
|
459
|
+
break;
|
|
460
|
+
case "ingest-started":
|
|
461
|
+
emitSyncEvent({ lifecycle: "sync", phase: "started" });
|
|
462
|
+
break;
|
|
463
|
+
case "ingest-complete": {
|
|
464
|
+
const { stored, failed } = countScopeResults(event.scopeResults);
|
|
465
|
+
emitSyncEvent({
|
|
466
|
+
lifecycle: "sync",
|
|
467
|
+
phase: "terminal",
|
|
468
|
+
outcome: "success",
|
|
469
|
+
storedScopeCount: stored,
|
|
470
|
+
failedScopeCount: failed,
|
|
471
|
+
});
|
|
472
|
+
syncRunId = null;
|
|
473
|
+
break;
|
|
390
474
|
}
|
|
391
|
-
|
|
475
|
+
case "ingest-partial": {
|
|
476
|
+
// Partial in the CLI means "some scopes stored, some failed" — in
|
|
477
|
+
// the canonical contract that is `outcome: success` with
|
|
478
|
+
// failedScopeCount > 0. The UI derives the "partial" label.
|
|
479
|
+
const { stored, failed } = countScopeResults(event.scopeResults);
|
|
480
|
+
emitSyncEvent({
|
|
481
|
+
lifecycle: "sync",
|
|
482
|
+
phase: "terminal",
|
|
483
|
+
outcome: "success",
|
|
484
|
+
storedScopeCount: stored,
|
|
485
|
+
failedScopeCount: failed,
|
|
486
|
+
});
|
|
487
|
+
syncRunId = null;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case "ingest-failed":
|
|
491
|
+
emitSyncEvent({
|
|
492
|
+
lifecycle: "sync",
|
|
493
|
+
phase: "terminal",
|
|
494
|
+
outcome: "failure",
|
|
495
|
+
errorClass: classifyCanonicalError("ingest_failed"),
|
|
496
|
+
});
|
|
497
|
+
syncRunId = null;
|
|
498
|
+
break;
|
|
499
|
+
case "ingest-skipped":
|
|
500
|
+
// Skip is standalone — no preceding `started` event.
|
|
501
|
+
syncRunId = randomId("sync"); // fresh ID for the standalone skip
|
|
502
|
+
emitSyncEvent({
|
|
503
|
+
lifecycle: "sync",
|
|
504
|
+
phase: "skipped",
|
|
505
|
+
reason: classifyCanonicalError(event.reason) ===
|
|
506
|
+
"personal_server_unavailable"
|
|
507
|
+
? "server_unavailable"
|
|
508
|
+
: "not_requested",
|
|
509
|
+
});
|
|
510
|
+
syncRunId = null;
|
|
511
|
+
break;
|
|
512
|
+
case "outcome":
|
|
513
|
+
latestOutcomeRaw = event.status ?? null;
|
|
514
|
+
break;
|
|
515
|
+
case "progress-update":
|
|
516
|
+
case "status-update":
|
|
517
|
+
case "headed-required":
|
|
518
|
+
case "jpeg":
|
|
519
|
+
// Not modeled in canonical telemetry.
|
|
520
|
+
break;
|
|
392
521
|
}
|
|
393
|
-
push(mapped.eventName, {
|
|
394
|
-
...mapped.patch,
|
|
395
|
-
source: event.source ?? context.source ?? null,
|
|
396
|
-
});
|
|
397
|
-
},
|
|
398
|
-
trackCustomEvent(eventName, patch = {}) {
|
|
399
|
-
push(eventName, patch);
|
|
400
522
|
},
|
|
401
523
|
markCommandResult(result) {
|
|
402
|
-
latestOutcome = result.outcome ?? latestOutcome;
|
|
403
|
-
latestErrorClass = result.errorClass ?? latestErrorClass;
|
|
404
524
|
const durationMs = Date.now() - startedAt;
|
|
525
|
+
latestOutcomeRaw = result.outcome ?? latestOutcomeRaw;
|
|
526
|
+
// If a collection run was started but never terminated, close it here.
|
|
527
|
+
// If the CLI exited with a non-zero code, record collection_failed;
|
|
528
|
+
// otherwise let the lifecycle's own terminal stand.
|
|
529
|
+
if (collectionRunId && collectionSource) {
|
|
530
|
+
const hasCollectionTerminal = events.some((e) => e.correlation.scope === "collection" &&
|
|
531
|
+
e.correlation.collectionRunId === collectionRunId &&
|
|
532
|
+
e.kind.lifecycle === "collection" &&
|
|
533
|
+
e.kind.phase === "terminal");
|
|
534
|
+
if (!hasCollectionTerminal) {
|
|
535
|
+
if (result.exitCode === 0) {
|
|
536
|
+
push(factory.build({
|
|
537
|
+
correlation: {
|
|
538
|
+
scope: "collection",
|
|
539
|
+
hostRunId,
|
|
540
|
+
collectionRunId,
|
|
541
|
+
source: collectionSource,
|
|
542
|
+
},
|
|
543
|
+
kind: {
|
|
544
|
+
lifecycle: "collection",
|
|
545
|
+
phase: "terminal",
|
|
546
|
+
outcome: "success",
|
|
547
|
+
},
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
push(factory.build({
|
|
552
|
+
correlation: {
|
|
553
|
+
scope: "collection",
|
|
554
|
+
hostRunId,
|
|
555
|
+
collectionRunId,
|
|
556
|
+
source: collectionSource,
|
|
557
|
+
},
|
|
558
|
+
kind: {
|
|
559
|
+
lifecycle: "collection",
|
|
560
|
+
phase: "terminal",
|
|
561
|
+
outcome: "failure",
|
|
562
|
+
errorClass: classifyCanonicalError(result.errorClass ?? latestOutcomeRaw),
|
|
563
|
+
},
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Host terminal.
|
|
405
569
|
if (result.exitCode === 0) {
|
|
406
|
-
push(
|
|
570
|
+
push(factory.build({
|
|
571
|
+
correlation: { scope: "host", hostRunId },
|
|
572
|
+
kind: { lifecycle: "host", phase: "terminal", outcome: "success" },
|
|
407
573
|
durationMs,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
push(factory.build({
|
|
578
|
+
correlation: { scope: "host", hostRunId },
|
|
579
|
+
kind: {
|
|
580
|
+
lifecycle: "host",
|
|
581
|
+
phase: "terminal",
|
|
582
|
+
outcome: "failure",
|
|
583
|
+
errorClass: classifyCanonicalError(result.errorClass ?? latestOutcomeRaw),
|
|
584
|
+
},
|
|
585
|
+
durationMs,
|
|
586
|
+
}));
|
|
412
587
|
}
|
|
413
|
-
push("command_failed", {
|
|
414
|
-
durationMs,
|
|
415
|
-
outcome: latestOutcome,
|
|
416
|
-
errorClass: latestErrorClass ?? "unknown",
|
|
417
|
-
});
|
|
418
588
|
},
|
|
419
589
|
async persist() {
|
|
420
|
-
if (persisted)
|
|
590
|
+
if (persisted)
|
|
421
591
|
return;
|
|
422
|
-
}
|
|
423
592
|
persisted = true;
|
|
424
593
|
if (state.mode === "debug") {
|
|
425
|
-
for (const envelope of splitIntoEnvelopes(events
|
|
594
|
+
for (const envelope of splitIntoEnvelopes(events)) {
|
|
426
595
|
process.stderr.write(`${JSON.stringify(envelope)}\n`);
|
|
427
596
|
}
|
|
428
597
|
return;
|
|
429
598
|
}
|
|
430
|
-
if (!state.enabled)
|
|
599
|
+
if (!state.enabled)
|
|
431
600
|
return;
|
|
432
|
-
|
|
433
|
-
const envelopes = splitIntoEnvelopes(events, context.cliVersion);
|
|
434
|
-
const runId = events[0]?.runId ?? randomId("run");
|
|
601
|
+
const envelopes = splitIntoEnvelopes(events);
|
|
435
602
|
for (const envelope of envelopes) {
|
|
436
|
-
await writeEnvelope(envelope,
|
|
603
|
+
await writeEnvelope(envelope, hostRunId);
|
|
437
604
|
}
|
|
438
605
|
},
|
|
606
|
+
trackCustomEvent() {
|
|
607
|
+
// No-op. See the CliTelemetrySession interface doc.
|
|
608
|
+
},
|
|
439
609
|
async flush() {
|
|
440
610
|
await flushTelemetryOutbox();
|
|
441
611
|
},
|
|
442
612
|
};
|
|
443
613
|
}
|
|
614
|
+
// ── Deprecated ──────────────────────────────────────────────────────────────
|
|
615
|
+
/** @deprecated trackCustomEvent on the session is a no-op in the canonical
|
|
616
|
+
* model. This top-level helper is also a no-op — kept only to satisfy
|
|
617
|
+
* existing call sites. */
|
|
618
|
+
export function trackActiveTelemetryEvent(_eventName, _patch) {
|
|
619
|
+
/* intentionally blank */
|
|
620
|
+
}
|
|
444
621
|
//# sourceMappingURL=telemetry.js.map
|