@opendatalabs/connect 0.12.2 → 0.13.1-canary.5f3c748
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/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 +388 -261
- package/dist/cli/telemetry.js.map +1 -1
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +4 -1
- package/dist/connectors/registry.js.map +1 -1
- package/dist/runtime/playwright/in-process-run.d.ts.map +1 -1
- package/dist/runtime/playwright/in-process-run.js +101 -46
- package/dist/runtime/playwright/in-process-run.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,177 +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") || value.includes("cancelled"))
|
|
48
|
-
return "prompt_cancelled";
|
|
49
|
-
if (value.includes("needs_input") ||
|
|
50
|
-
value.includes("needs input") ||
|
|
51
|
-
value.includes("input required") ||
|
|
52
|
-
value.includes("manual step"))
|
|
53
|
-
return "needs_input";
|
|
54
|
-
if (value.includes("auth expired"))
|
|
55
|
-
return "auth_expired";
|
|
56
|
-
if (value.includes("auth"))
|
|
57
|
-
return "auth_failed";
|
|
58
|
-
if (value.includes("legacy"))
|
|
59
|
-
return "legacy_auth";
|
|
60
|
-
if (value.includes("personal_server_unavailable") ||
|
|
61
|
-
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")) {
|
|
62
51
|
return "personal_server_unavailable";
|
|
63
|
-
|
|
52
|
+
}
|
|
53
|
+
if (normalized.includes("auth") || normalized.includes("legacy")) {
|
|
54
|
+
return "auth_failed";
|
|
55
|
+
}
|
|
56
|
+
if (normalized.includes("timeout") || normalized.includes("timed out")) {
|
|
64
57
|
return "timeout";
|
|
65
|
-
|
|
58
|
+
}
|
|
59
|
+
if (normalized.includes("network")) {
|
|
66
60
|
return "network_error";
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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")) {
|
|
72
68
|
return "runtime_error";
|
|
73
|
-
return "unknown";
|
|
74
|
-
}
|
|
75
|
-
function classifyOutcomeStatus(outcome) {
|
|
76
|
-
switch (outcome) {
|
|
77
|
-
case "needs_input":
|
|
78
|
-
return "needs_input";
|
|
79
|
-
case "auth_failed":
|
|
80
|
-
return "auth_failed";
|
|
81
|
-
case "legacy_auth":
|
|
82
|
-
return "legacy_auth";
|
|
83
|
-
case "personal_server_unavailable":
|
|
84
|
-
return "personal_server_unavailable";
|
|
85
|
-
case "ingest_failed":
|
|
86
|
-
return "ingest_failed";
|
|
87
|
-
case "connector_unavailable":
|
|
88
|
-
return "connector_unavailable";
|
|
89
|
-
case "runtime_error":
|
|
90
|
-
return "runtime_error";
|
|
91
|
-
case "setup_required":
|
|
92
|
-
return "setup_required";
|
|
93
|
-
case "setup_declined":
|
|
94
|
-
return "setup_declined";
|
|
95
|
-
case "source_required":
|
|
96
|
-
return "source_required";
|
|
97
|
-
case "prompt_cancelled":
|
|
98
|
-
return "prompt_cancelled";
|
|
99
|
-
case "invalid_connector":
|
|
100
|
-
return "invalid_connector";
|
|
101
|
-
case "unexpected_internal_error":
|
|
102
|
-
return "unexpected_internal_error";
|
|
103
|
-
default:
|
|
104
|
-
return null;
|
|
105
69
|
}
|
|
70
|
+
return "unknown";
|
|
106
71
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
eventName: "runtime_check_completed",
|
|
120
|
-
patch: {
|
|
121
|
-
metadata: sanitizeMetadata({ runtime: event.runtime ?? null }),
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
case "setup-complete":
|
|
125
|
-
return {
|
|
126
|
-
eventName: "runtime_install_completed",
|
|
127
|
-
patch: {
|
|
128
|
-
metadata: sanitizeMetadata({ runtime: event.runtime ?? null }),
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
case "connector-resolved":
|
|
132
|
-
return { eventName: "connector_resolved" };
|
|
133
|
-
case "needs-input":
|
|
134
|
-
return {
|
|
135
|
-
eventName: "input_required",
|
|
136
|
-
patch: {
|
|
137
|
-
errorClass: "needs_input",
|
|
138
|
-
metadata: sanitizeMetadata({ fieldCount: event.fields?.length ?? 0 }),
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
case "legacy-auth":
|
|
142
|
-
return {
|
|
143
|
-
eventName: "legacy_auth_required",
|
|
144
|
-
patch: { errorClass: "legacy_auth" },
|
|
145
|
-
};
|
|
146
|
-
case "collection-complete":
|
|
147
|
-
return { eventName: "collection_completed" };
|
|
148
|
-
case "runtime-error":
|
|
149
|
-
return {
|
|
150
|
-
eventName: "collection_failed",
|
|
151
|
-
patch: { errorClass: classifyError(event.message) ?? "runtime_error" },
|
|
152
|
-
};
|
|
153
|
-
case "ingest-started":
|
|
154
|
-
return { eventName: "ingest_started" };
|
|
155
|
-
case "ingest-complete":
|
|
156
|
-
return {
|
|
157
|
-
eventName: "ingest_completed",
|
|
158
|
-
patch: countScopeResults(event.scopeResults),
|
|
159
|
-
};
|
|
160
|
-
case "ingest-partial":
|
|
161
|
-
return {
|
|
162
|
-
eventName: "ingest_partial",
|
|
163
|
-
patch: {
|
|
164
|
-
...countScopeResults(event.scopeResults),
|
|
165
|
-
errorClass: "ingest_failed",
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
case "ingest-failed":
|
|
169
|
-
return {
|
|
170
|
-
eventName: "ingest_failed",
|
|
171
|
-
patch: {
|
|
172
|
-
...countScopeResults(event.scopeResults),
|
|
173
|
-
errorClass: "ingest_failed",
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
case "ingest-skipped":
|
|
177
|
-
return {
|
|
178
|
-
eventName: "ingest_skipped",
|
|
179
|
-
patch: {
|
|
180
|
-
errorClass: classifyError(event.reason) ?? null,
|
|
181
|
-
metadata: sanitizeMetadata({ reason: event.reason ?? null }),
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
case "outcome":
|
|
185
|
-
case "progress-update":
|
|
186
|
-
case "status-update":
|
|
187
|
-
case "headed-required":
|
|
188
|
-
case "jpeg":
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
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";
|
|
192
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.
|
|
193
91
|
async function ensureOutboxDir() {
|
|
194
92
|
await fs.mkdir(getTelemetryOutboxDir(), { recursive: true });
|
|
195
93
|
}
|
|
@@ -272,52 +170,64 @@ async function resolveTelemetryState(localOnly = false) {
|
|
|
272
170
|
queuedBatches,
|
|
273
171
|
};
|
|
274
172
|
}
|
|
173
|
+
// ── Event factory ───────────────────────────────────────────────────────────
|
|
174
|
+
//
|
|
175
|
+
// Builds canonical TelemetryEvent values with identity + time + attribution +
|
|
176
|
+
// context auto-populated. The caller supplies correlation + kind.
|
|
275
177
|
function createEventFactory(context, state) {
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
},
|
|
294
219
|
};
|
|
295
|
-
return (eventName, patch = {}) => ({
|
|
296
|
-
eventId: randomId("evt"),
|
|
297
|
-
eventVersion: EVENT_VERSION,
|
|
298
|
-
timestamp: nowIso(),
|
|
299
|
-
eventName,
|
|
300
|
-
...base,
|
|
301
|
-
...patch,
|
|
302
|
-
outcome: patch.outcome ?? null,
|
|
303
|
-
errorClass: patch.errorClass ?? null,
|
|
304
|
-
durationMs: patch.durationMs ?? null,
|
|
305
|
-
storedScopeCount: patch.storedScopeCount ?? null,
|
|
306
|
-
failedScopeCount: patch.failedScopeCount ?? null,
|
|
307
|
-
metadata: sanitizeMetadata(patch.metadata),
|
|
308
|
-
});
|
|
309
220
|
}
|
|
310
|
-
|
|
221
|
+
// ── Batch serialization ─────────────────────────────────────────────────────
|
|
222
|
+
function splitIntoEnvelopes(events) {
|
|
311
223
|
const envelopes = [];
|
|
312
224
|
let current = [];
|
|
313
225
|
const flushCurrent = () => {
|
|
314
|
-
if (current.length === 0)
|
|
226
|
+
if (current.length === 0)
|
|
315
227
|
return;
|
|
316
|
-
}
|
|
317
228
|
envelopes.push({
|
|
318
229
|
batchId: randomId("batch"),
|
|
319
230
|
sentAt: nowIso(),
|
|
320
|
-
client: { name: "vana-cli", version: cliVersion },
|
|
321
231
|
events: current,
|
|
322
232
|
});
|
|
323
233
|
current = [];
|
|
@@ -327,7 +237,6 @@ function splitIntoEnvelopes(events, cliVersion) {
|
|
|
327
237
|
const candidate = {
|
|
328
238
|
batchId: "batch_candidate",
|
|
329
239
|
sentAt: nowIso(),
|
|
330
|
-
client: { name: "vana-cli", version: cliVersion },
|
|
331
240
|
events: current,
|
|
332
241
|
};
|
|
333
242
|
const tooManyEvents = current.length > MAX_EVENTS_PER_BATCH;
|
|
@@ -335,25 +244,23 @@ function splitIntoEnvelopes(events, cliVersion) {
|
|
|
335
244
|
if (tooManyEvents || tooLarge) {
|
|
336
245
|
const overflow = current.pop();
|
|
337
246
|
flushCurrent();
|
|
338
|
-
if (overflow)
|
|
247
|
+
if (overflow)
|
|
339
248
|
current.push(overflow);
|
|
340
|
-
}
|
|
341
249
|
}
|
|
342
250
|
}
|
|
343
251
|
flushCurrent();
|
|
344
252
|
return envelopes;
|
|
345
253
|
}
|
|
346
|
-
async function writeEnvelope(envelope,
|
|
254
|
+
async function writeEnvelope(envelope, hostRunId) {
|
|
347
255
|
await ensureOutboxDir();
|
|
348
|
-
const filename = `${Date.now()}-${process.pid}-${
|
|
256
|
+
const filename = `${Date.now()}-${process.pid}-${hostRunId}-${crypto.randomUUID()}.json`;
|
|
349
257
|
const outboxPath = path.join(getTelemetryOutboxDir(), filename);
|
|
350
258
|
await fs.writeFile(outboxPath, `${JSON.stringify(envelope)}\n`, "utf8");
|
|
351
259
|
}
|
|
352
260
|
export async function flushTelemetryOutbox() {
|
|
353
261
|
const state = await resolveTelemetryState();
|
|
354
|
-
if (!state.enabled || state.mode !== "normal")
|
|
262
|
+
if (!state.enabled || state.mode !== "normal")
|
|
355
263
|
return;
|
|
356
|
-
}
|
|
357
264
|
const files = (await listOutboxFiles()).slice(0, MAX_FILES_PER_FLUSH);
|
|
358
265
|
for (const filePath of files) {
|
|
359
266
|
let contents;
|
|
@@ -368,7 +275,7 @@ export async function flushTelemetryOutbox() {
|
|
|
368
275
|
method: "POST",
|
|
369
276
|
headers: {
|
|
370
277
|
"Content-Type": "application/json",
|
|
371
|
-
"User-Agent": `vana-cli
|
|
278
|
+
"User-Agent": `vana-cli/unknown`,
|
|
372
279
|
},
|
|
373
280
|
body: contents,
|
|
374
281
|
signal: AbortSignal.timeout(FLUSH_TIMEOUT_MS),
|
|
@@ -382,6 +289,7 @@ export async function flushTelemetryOutbox() {
|
|
|
382
289
|
}
|
|
383
290
|
}
|
|
384
291
|
}
|
|
292
|
+
// ── Public helpers ──────────────────────────────────────────────────────────
|
|
385
293
|
export async function getTelemetryStatus() {
|
|
386
294
|
const state = await resolveTelemetryState();
|
|
387
295
|
return {
|
|
@@ -402,93 +310,312 @@ export function setActiveTelemetrySession(session) {
|
|
|
402
310
|
export function getActiveTelemetrySession() {
|
|
403
311
|
return activeSession;
|
|
404
312
|
}
|
|
405
|
-
|
|
406
|
-
activeSession?.trackCustomEvent(eventName, patch);
|
|
407
|
-
}
|
|
313
|
+
// ── Session ─────────────────────────────────────────────────────────────────
|
|
408
314
|
export async function createCliTelemetrySession(context) {
|
|
409
315
|
const state = await resolveTelemetryState(Boolean(context.localOnly));
|
|
410
|
-
const
|
|
316
|
+
const factory = createEventFactory(context, state);
|
|
411
317
|
const startedAt = Date.now();
|
|
412
318
|
const events = [];
|
|
413
|
-
|
|
414
|
-
|
|
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;
|
|
415
328
|
let persisted = false;
|
|
416
|
-
|
|
417
|
-
|
|
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")
|
|
418
342
|
return;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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 };
|
|
422
408
|
};
|
|
423
|
-
push("command_started", {
|
|
424
|
-
metadata: sanitizeMetadata({
|
|
425
|
-
launchMode: context.options.detach ? "detached" : "direct",
|
|
426
|
-
inputMode: context.options.ipc
|
|
427
|
-
? "ipc"
|
|
428
|
-
: context.options.noInput
|
|
429
|
-
? "no_input"
|
|
430
|
-
: "interactive",
|
|
431
|
-
}),
|
|
432
|
-
});
|
|
433
409
|
return {
|
|
434
410
|
trackCliEvent(event) {
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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;
|
|
440
474
|
}
|
|
441
|
-
|
|
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;
|
|
442
521
|
}
|
|
443
|
-
push(mapped.eventName, {
|
|
444
|
-
...mapped.patch,
|
|
445
|
-
source: event.source ?? context.source ?? null,
|
|
446
|
-
});
|
|
447
|
-
},
|
|
448
|
-
trackCustomEvent(eventName, patch = {}) {
|
|
449
|
-
push(eventName, patch);
|
|
450
522
|
},
|
|
451
523
|
markCommandResult(result) {
|
|
452
|
-
latestOutcome = result.outcome ?? latestOutcome;
|
|
453
|
-
latestErrorClass = resolveErrorClass(result.errorClass, latestErrorClass, classifyOutcomeStatus(latestOutcome));
|
|
454
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.
|
|
455
569
|
if (result.exitCode === 0) {
|
|
456
|
-
push(
|
|
570
|
+
push(factory.build({
|
|
571
|
+
correlation: { scope: "host", hostRunId },
|
|
572
|
+
kind: { lifecycle: "host", phase: "terminal", outcome: "success" },
|
|
457
573
|
durationMs,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
}));
|
|
462
587
|
}
|
|
463
|
-
push("command_failed", {
|
|
464
|
-
durationMs,
|
|
465
|
-
outcome: latestOutcome,
|
|
466
|
-
errorClass: latestErrorClass ?? "unknown",
|
|
467
|
-
});
|
|
468
588
|
},
|
|
469
589
|
async persist() {
|
|
470
|
-
if (persisted)
|
|
590
|
+
if (persisted)
|
|
471
591
|
return;
|
|
472
|
-
}
|
|
473
592
|
persisted = true;
|
|
474
593
|
if (state.mode === "debug") {
|
|
475
|
-
for (const envelope of splitIntoEnvelopes(events
|
|
594
|
+
for (const envelope of splitIntoEnvelopes(events)) {
|
|
476
595
|
process.stderr.write(`${JSON.stringify(envelope)}\n`);
|
|
477
596
|
}
|
|
478
597
|
return;
|
|
479
598
|
}
|
|
480
|
-
if (!state.enabled)
|
|
599
|
+
if (!state.enabled)
|
|
481
600
|
return;
|
|
482
|
-
|
|
483
|
-
const envelopes = splitIntoEnvelopes(events, context.cliVersion);
|
|
484
|
-
const runId = events[0]?.runId ?? randomId("run");
|
|
601
|
+
const envelopes = splitIntoEnvelopes(events);
|
|
485
602
|
for (const envelope of envelopes) {
|
|
486
|
-
await writeEnvelope(envelope,
|
|
603
|
+
await writeEnvelope(envelope, hostRunId);
|
|
487
604
|
}
|
|
488
605
|
},
|
|
606
|
+
trackCustomEvent() {
|
|
607
|
+
// No-op. See the CliTelemetrySession interface doc.
|
|
608
|
+
},
|
|
489
609
|
async flush() {
|
|
490
610
|
await flushTelemetryOutbox();
|
|
491
611
|
},
|
|
492
612
|
};
|
|
493
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
|
+
}
|
|
494
621
|
//# sourceMappingURL=telemetry.js.map
|