@vellumai/assistant 0.10.9-staging.1 → 0.10.9-staging.2
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/AGENTS.md +4 -0
- package/knip.json +1 -4
- package/openapi.yaml +28 -18
- package/package.json +1 -1
- package/src/__tests__/always-loaded-tools-guard.test.ts +4 -6
- package/src/__tests__/app-routes-csp.test.ts +13 -4
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +4 -8
- package/src/__tests__/disk-pressure-tools.test.ts +4 -4
- package/src/__tests__/file-list-tool.test.ts +22 -12
- package/src/__tests__/list-all-apps.test.ts +204 -0
- package/src/__tests__/messaging-send-tool.test.ts +67 -0
- package/src/__tests__/mtime-cache.test.ts +42 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +3 -2
- package/src/__tests__/outlook-messaging-provider.test.ts +71 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +163 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +47 -0
- package/src/__tests__/subagent-notify-parent.test.ts +4 -3
- package/src/__tests__/subagent-tool-filtering.test.ts +21 -21
- package/src/__tests__/subagent-tool-gate-mode.test.ts +6 -6
- package/src/apps/app-store.ts +302 -2
- package/src/cli/AGENTS.md +17 -0
- package/src/cli/commands/__tests__/gateway.test.ts +55 -0
- package/src/cli/commands/apps.help.ts +17 -11
- package/src/cli/commands/apps.ts +5 -15
- package/src/cli/commands/gateway.help.ts +30 -0
- package/src/cli/commands/gateway.ts +40 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -90
- package/src/cli/commands/platform/index.help.ts +3 -2
- package/src/cli/commands/platform/index.ts +0 -9
- package/src/cli/commands/routes.help.ts +13 -7
- package/src/cli/commands/routes.ts +5 -3
- package/src/cli/commands/status.help.ts +24 -0
- package/src/cli/commands/status.ts +29 -0
- package/src/cli/lib/__tests__/global-json-option.test.ts +97 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +1 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +31 -0
- package/src/cli/lib/bundled-marketplace.json +187 -173
- package/src/cli/lib/global-json-option.ts +41 -0
- package/src/cli/lib/plugin-fingerprint.ts +18 -7
- package/src/cli/program.ts +5 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +27 -19
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/feature-flag-registry.json +1 -9
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +3 -6
- package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +3 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +8 -5
- package/src/daemon/conversation-tool-setup.ts +5 -90
- package/src/daemon/conversation.ts +2 -2
- package/src/messaging/provider-types.ts +9 -0
- package/src/messaging/providers/outlook/adapter.ts +17 -2
- package/src/messaging/providers/outlook/client.ts +11 -2
- package/src/messaging/providers/outlook/types.ts +9 -0
- package/src/monitoring/__tests__/plugin-source-watch.test.ts +37 -0
- package/src/monitoring/plugin-source-watch.ts +53 -0
- package/src/oauth/seed-providers.ts +6 -0
- package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +3 -0
- package/src/persistence/embeddings/messages-lexical-index.ts +11 -0
- package/src/plugin-api/conversation-turn.ts +234 -0
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/__tests__/source-fingerprint.test.ts +41 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +48 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +46 -5
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +2 -2
- package/src/plugins/defaults/memory/graph-topology/__tests__/build-memory-graph.test.ts +81 -2
- package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +80 -33
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +40 -1
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +28 -2
- package/src/plugins/mtime-cache.ts +26 -13
- package/src/plugins/plugin-tree-walk.ts +30 -0
- package/src/plugins/source-fingerprint.ts +9 -2
- package/src/plugins/surface-import.ts +28 -0
- package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +1 -1
- package/src/providers/speech-to-text/__tests__/vellum-speech-relay-connection.test.ts +4 -1
- package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +5 -0
- package/src/runtime/routes/__tests__/gateway-status-routes.test.ts +84 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +47 -3
- package/src/runtime/routes/__tests__/user-routes-cli.test.ts +154 -0
- package/src/runtime/routes/app-management-routes.ts +96 -32
- package/src/runtime/routes/app-routes.ts +33 -17
- package/src/runtime/routes/gateway-status-routes.ts +69 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +4 -14
- package/src/runtime/routes/plugins-routes.ts +58 -1
- package/src/runtime/routes/settings-routes.ts +8 -6
- package/src/runtime/routes/user-route-dispatcher.ts +6 -86
- package/src/runtime/routes/user-route-resolution.ts +141 -0
- package/src/runtime/routes/user-routes-cli.ts +56 -35
- package/src/telemetry/AGENTS.md +15 -0
- package/src/telemetry/__tests__/telemetry-event-fixtures.ts +258 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire-validation.test.ts +157 -0
- package/src/telemetry/telemetry-wire-validation.ts +125 -0
- package/src/telemetry/telemetry-wire.generated.ts +60 -9
- package/src/telemetry/types.test.ts +38 -0
- package/src/telemetry/types.ts +183 -91
- package/src/telemetry/usage-telemetry-reporter.ts +5 -0
- package/src/telemetry/watchdog-direct-emit.ts +6 -0
- package/src/tools/skills/scaffold-managed.ts +43 -11
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flush validation of outgoing telemetry events against the
|
|
3
|
+
* platform-generated wire schemas (`telemetry-wire.generated.ts`).
|
|
4
|
+
*
|
|
5
|
+
* The platform ingest endpoint rejects individual events that violate its
|
|
6
|
+
* serializer bounds and skips event types it has no serializer for — both
|
|
7
|
+
* silently from the daemon's point of view (the batch still 2xxes). This
|
|
8
|
+
* module surfaces those would-be-silent drops as structured warnings at the
|
|
9
|
+
* source, right before each POST.
|
|
10
|
+
*
|
|
11
|
+
* Observability only: validation never mutates, filters, or blocks a batch.
|
|
12
|
+
* The server remains the authority on what it accepts; in particular the
|
|
13
|
+
* `.trim()`-transformed parse output is never substituted for the original
|
|
14
|
+
* events.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { z } from "zod";
|
|
18
|
+
|
|
19
|
+
import type { getLogger } from "../util/logger.js";
|
|
20
|
+
import { telemetryEventSchema } from "./telemetry-wire.generated.js";
|
|
21
|
+
|
|
22
|
+
type Logger = ReturnType<typeof getLogger>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Per-event-type wire schema, derived by introspecting the generated
|
|
26
|
+
* discriminated union (`.options` + each member's `type` literal), so a new
|
|
27
|
+
* generated event type is validated with zero hand edits here.
|
|
28
|
+
*/
|
|
29
|
+
const wireSchemaByType: ReadonlyMap<string, z.ZodType> = new Map(
|
|
30
|
+
telemetryEventSchema.options.map((option) => [
|
|
31
|
+
option.shape.type.value,
|
|
32
|
+
option,
|
|
33
|
+
]),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Event types already warned about as absent from the wire contract.
|
|
38
|
+
* Unknown-type warnings are rate-limited to once per process per type —
|
|
39
|
+
* without this, a daemon-only extension type (e.g. `onboarding_research`)
|
|
40
|
+
* would warn on every 5-minute flush forever.
|
|
41
|
+
*/
|
|
42
|
+
const warnedUnknownTypes = new Set<string>();
|
|
43
|
+
|
|
44
|
+
/** Clears the once-per-process unknown-type warning rate limit. Test-only. */
|
|
45
|
+
export function resetUnknownTypeWarningsForTests(): void {
|
|
46
|
+
warnedUnknownTypes.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Render a zod issue path for logging without leaking dynamic record keys.
|
|
51
|
+
*
|
|
52
|
+
* Rule: keep numeric components (array indices — structural, never data) and
|
|
53
|
+
* keep the first (depth-0) component; redact every deeper string component to
|
|
54
|
+
* a literal `*`. Depth-0 is safe because every generated event schema is a
|
|
55
|
+
* flat `z.object`, so top-level path components are schema-defined field
|
|
56
|
+
* names. Deeper string components can be dynamic record keys — e.g. the turn
|
|
57
|
+
* schema's `client` bag superRefine emits issues at `['client', <key>]` where
|
|
58
|
+
* `<key>` comes from an uncontrolled JSON metadata column and could carry
|
|
59
|
+
* user text.
|
|
60
|
+
*/
|
|
61
|
+
function sanitizeIssuePath(path: ReadonlyArray<PropertyKey>): string {
|
|
62
|
+
return path
|
|
63
|
+
.map((component, depth) =>
|
|
64
|
+
typeof component === "number" || depth === 0 ? String(component) : "*",
|
|
65
|
+
)
|
|
66
|
+
.join(".");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface WireValidationResult {
|
|
70
|
+
/** Events whose type had a wire schema and were parsed against it. */
|
|
71
|
+
checked: number;
|
|
72
|
+
/** Checked events that failed their wire schema. */
|
|
73
|
+
invalid: number;
|
|
74
|
+
/** Distinct event types with no wire schema — the server drops these. */
|
|
75
|
+
unknownTypes: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate a batch of outgoing telemetry events against the platform wire
|
|
80
|
+
* schemas, logging a structured warning for each event the server would
|
|
81
|
+
* silently drop. Warn payloads carry the event `type` and issue
|
|
82
|
+
* `{ path, code }` shapes only — never field values, and never dynamic key
|
|
83
|
+
* components inside paths (see {@link sanitizeIssuePath}). That includes
|
|
84
|
+
* `daemon_event_id`: traces/claims can hold PII, and activation-funnel ids
|
|
85
|
+
* embed the onboarding session id.
|
|
86
|
+
*
|
|
87
|
+
* Never mutates, filters, or blocks: callers send the batch unchanged
|
|
88
|
+
* regardless of the result.
|
|
89
|
+
*/
|
|
90
|
+
export function validateWireEvents(
|
|
91
|
+
events: readonly { type: string }[],
|
|
92
|
+
log: Logger,
|
|
93
|
+
): WireValidationResult {
|
|
94
|
+
let checked = 0;
|
|
95
|
+
let invalid = 0;
|
|
96
|
+
const unknownTypes = new Set<string>();
|
|
97
|
+
for (const event of events) {
|
|
98
|
+
const schema = wireSchemaByType.get(event.type);
|
|
99
|
+
if (!schema) {
|
|
100
|
+
unknownTypes.add(event.type);
|
|
101
|
+
if (!warnedUnknownTypes.has(event.type)) {
|
|
102
|
+
warnedUnknownTypes.add(event.type);
|
|
103
|
+
log.warn(
|
|
104
|
+
{ eventType: event.type },
|
|
105
|
+
"telemetry event type not in platform wire contract — server drops these; see telemetry-wire.generated.ts",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
checked += 1;
|
|
111
|
+
const result = schema.safeParse(event);
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
invalid += 1;
|
|
114
|
+
const issues = result.error.issues.map((issue) => ({
|
|
115
|
+
path: sanitizeIssuePath(issue.path),
|
|
116
|
+
code: issue.code,
|
|
117
|
+
}));
|
|
118
|
+
log.warn(
|
|
119
|
+
{ eventType: event.type, issues },
|
|
120
|
+
"telemetry event fails platform wire contract — server will silently drop it",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { checked, invalid, unknownTypes: [...unknownTypes] };
|
|
125
|
+
}
|
|
@@ -57,10 +57,12 @@ export const TURN_CLIENT_MAX_JSON_BYTES = 2048;
|
|
|
57
57
|
// pre-trim a trace it would rather not lose.
|
|
58
58
|
export const TURN_TRACE_MAX_JSON_BYTES = 262144;
|
|
59
59
|
export const WATCHDOG_DETAIL_MAX_JSON_BYTES = 4096;
|
|
60
|
+
export const ONBOARDING_RESEARCH_CLAIMS_MAX_JSON_BYTES = 8192;
|
|
61
|
+
export const ONBOARDING_RESEARCH_SUGGESTIONS_MAX_JSON_BYTES = 8192;
|
|
60
62
|
|
|
61
63
|
export const llmUsageTelemetryEventSchema = z.object({
|
|
62
64
|
type: z.literal("llm_usage"),
|
|
63
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
65
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
64
66
|
recorded_at: z.number().int(),
|
|
65
67
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
66
68
|
provider: z.string().trim().min(1).max(64),
|
|
@@ -93,7 +95,7 @@ export type LlmUsageTelemetryEvent = z.infer<
|
|
|
93
95
|
export const turnTelemetryEventSchema = z
|
|
94
96
|
.object({
|
|
95
97
|
type: z.literal("turn"),
|
|
96
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
98
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
97
99
|
recorded_at: z.number().int(),
|
|
98
100
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
99
101
|
conversation_type: z.string().trim().min(1).max(32).nullable().optional(),
|
|
@@ -160,7 +162,7 @@ export type TurnTelemetryEvent = z.infer<typeof turnTelemetryEventSchema>;
|
|
|
160
162
|
|
|
161
163
|
export const lifecycleTelemetryEventSchema = z.object({
|
|
162
164
|
type: z.literal("lifecycle"),
|
|
163
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
165
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
164
166
|
recorded_at: z.number().int(),
|
|
165
167
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
166
168
|
event_name: z.string().trim().min(1).max(64),
|
|
@@ -172,7 +174,7 @@ export type LifecycleTelemetryEvent = z.infer<
|
|
|
172
174
|
export const onboardingTelemetryEventSchema = z
|
|
173
175
|
.object({
|
|
174
176
|
type: z.literal("onboarding"),
|
|
175
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
177
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
176
178
|
recorded_at: z.number().int(),
|
|
177
179
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
178
180
|
screen: z.string().trim().min(1).max(64).optional(),
|
|
@@ -218,7 +220,7 @@ export type OnboardingTelemetryEvent = z.infer<
|
|
|
218
220
|
|
|
219
221
|
export const authFallbackTelemetryEventSchema = z.object({
|
|
220
222
|
type: z.literal("auth_fallback"),
|
|
221
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
223
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
222
224
|
recorded_at: z.number().int(),
|
|
223
225
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
224
226
|
guard: z.string().trim().min(1).max(64),
|
|
@@ -244,7 +246,7 @@ export const toolExecutedTelemetryEventSchema = z.object({
|
|
|
244
246
|
.nullable()
|
|
245
247
|
.optional(),
|
|
246
248
|
type: z.literal("tool_executed"),
|
|
247
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
249
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
248
250
|
recorded_at: z.number().int(),
|
|
249
251
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
250
252
|
tool_name: z.string().trim().min(1).max(255),
|
|
@@ -270,7 +272,7 @@ export const skillLoadedTelemetryEventSchema = z.object({
|
|
|
270
272
|
.nullable()
|
|
271
273
|
.optional(),
|
|
272
274
|
type: z.literal("skill_loaded"),
|
|
273
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
275
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
274
276
|
recorded_at: z.number().int(),
|
|
275
277
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
276
278
|
skill_name: z.string().trim().min(1).max(255),
|
|
@@ -284,7 +286,7 @@ export type SkillLoadedTelemetryEvent = z.infer<
|
|
|
284
286
|
export const watchdogTelemetryEventSchema = z
|
|
285
287
|
.object({
|
|
286
288
|
type: z.literal("watchdog"),
|
|
287
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
289
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
288
290
|
recorded_at: z.number().int(),
|
|
289
291
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
290
292
|
check_name: z.string().trim().min(1).max(128),
|
|
@@ -312,7 +314,7 @@ export type WatchdogTelemetryEvent = z.infer<
|
|
|
312
314
|
|
|
313
315
|
export const configSettingTelemetryEventSchema = z.object({
|
|
314
316
|
type: z.literal("config_setting"),
|
|
315
|
-
daemon_event_id: z.string().trim().min(1).max(
|
|
317
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
316
318
|
recorded_at: z.number().int(),
|
|
317
319
|
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
318
320
|
config_key: z.string().trim().min(1).max(128),
|
|
@@ -322,6 +324,53 @@ export type ConfigSettingTelemetryEvent = z.infer<
|
|
|
322
324
|
typeof configSettingTelemetryEventSchema
|
|
323
325
|
>;
|
|
324
326
|
|
|
327
|
+
export const onboardingResearchTelemetryEventSchema = z
|
|
328
|
+
.object({
|
|
329
|
+
type: z.literal("onboarding_research"),
|
|
330
|
+
daemon_event_id: z.string().trim().min(1).max(128),
|
|
331
|
+
recorded_at: z.number().int(),
|
|
332
|
+
assistant_version: z.string().trim().min(1).max(64).nullable().optional(),
|
|
333
|
+
conversation_id: z.string().trim().min(1).max(64).nullable().optional(),
|
|
334
|
+
status: z.string().trim().min(1).max(32),
|
|
335
|
+
claims: z.array(jsonValueSchema).max(20),
|
|
336
|
+
claim_count: z.number().int().min(0),
|
|
337
|
+
claims_confident: z.number().int().min(0),
|
|
338
|
+
claims_maybe: z.number().int().min(0),
|
|
339
|
+
claims_guessing: z.number().int().min(0),
|
|
340
|
+
suggestions: z.array(jsonValueSchema).max(20),
|
|
341
|
+
suggestion_count: z.number().int().min(0),
|
|
342
|
+
plugins: z.array(z.string().trim().min(1).max(128)).max(20),
|
|
343
|
+
installed_plugins: z.array(z.string().trim().min(1).max(128)).max(20),
|
|
344
|
+
})
|
|
345
|
+
.superRefine((val, ctx) => {
|
|
346
|
+
// Mirrors the server's OnboardingResearchTelemetryEventSerializer
|
|
347
|
+
// validate_claims / validate_suggestions — an oversize `claims` or
|
|
348
|
+
// `suggestions` array is rejected by ingest (the event is silently
|
|
349
|
+
// dropped, never the whole batch).
|
|
350
|
+
if (
|
|
351
|
+
jsonByteLength(val.claims) > ONBOARDING_RESEARCH_CLAIMS_MAX_JSON_BYTES
|
|
352
|
+
) {
|
|
353
|
+
ctx.addIssue({
|
|
354
|
+
code: "custom",
|
|
355
|
+
path: ["claims"],
|
|
356
|
+
message: `\`claims\` JSON exceeds ${ONBOARDING_RESEARCH_CLAIMS_MAX_JSON_BYTES} bytes when serialized.`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (
|
|
360
|
+
jsonByteLength(val.suggestions) >
|
|
361
|
+
ONBOARDING_RESEARCH_SUGGESTIONS_MAX_JSON_BYTES
|
|
362
|
+
) {
|
|
363
|
+
ctx.addIssue({
|
|
364
|
+
code: "custom",
|
|
365
|
+
path: ["suggestions"],
|
|
366
|
+
message: `\`suggestions\` JSON exceeds ${ONBOARDING_RESEARCH_SUGGESTIONS_MAX_JSON_BYTES} bytes when serialized.`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
export type OnboardingResearchTelemetryEvent = z.infer<
|
|
371
|
+
typeof onboardingResearchTelemetryEventSchema
|
|
372
|
+
>;
|
|
373
|
+
|
|
325
374
|
export type WireEventMap = {
|
|
326
375
|
llm_usage: LlmUsageTelemetryEvent;
|
|
327
376
|
turn: TurnTelemetryEvent;
|
|
@@ -332,6 +381,7 @@ export type WireEventMap = {
|
|
|
332
381
|
skill_loaded: SkillLoadedTelemetryEvent;
|
|
333
382
|
watchdog: WatchdogTelemetryEvent;
|
|
334
383
|
config_setting: ConfigSettingTelemetryEvent;
|
|
384
|
+
onboarding_research: OnboardingResearchTelemetryEvent;
|
|
335
385
|
};
|
|
336
386
|
|
|
337
387
|
export const telemetryEventSchema = z.discriminatedUnion("type", [
|
|
@@ -344,6 +394,7 @@ export const telemetryEventSchema = z.discriminatedUnion("type", [
|
|
|
344
394
|
skillLoadedTelemetryEventSchema,
|
|
345
395
|
watchdogTelemetryEventSchema,
|
|
346
396
|
configSettingTelemetryEventSchema,
|
|
397
|
+
onboardingResearchTelemetryEventSchema,
|
|
347
398
|
]);
|
|
348
399
|
export type TelemetryEvent = z.infer<typeof telemetryEventSchema>;
|
|
349
400
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
turnEventSample,
|
|
5
|
+
wireEventSamples,
|
|
6
|
+
} from "./__tests__/telemetry-event-fixtures.js";
|
|
7
|
+
import { telemetryEventSchema } from "./telemetry-wire.generated.js";
|
|
8
|
+
import type { TurnTelemetryEvent } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// Runtime contract test: events constructed with the daemon's types (the
|
|
11
|
+
// shared fixtures are annotated with them) must parse against the generated
|
|
12
|
+
// wire schemas — the same validation the platform's ingest serializers
|
|
13
|
+
// apply. A daemon type whose values can't round-trip through
|
|
14
|
+
// `telemetryEventSchema` produces events the server silently drops.
|
|
15
|
+
|
|
16
|
+
describe("daemon telemetry types against the wire contract", () => {
|
|
17
|
+
for (const sample of wireEventSamples) {
|
|
18
|
+
test(`daemon-typed ${sample.type} event parses against the wire schema`, () => {
|
|
19
|
+
const result = telemetryEventSchema.safeParse(sample);
|
|
20
|
+
expect(result.error).toBeUndefined();
|
|
21
|
+
expect(result.success).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("turn client bag with a nested value fails the wire superRefine", () => {
|
|
26
|
+
// Structurally valid for the daemon's `TurnTelemetryClientInfo` (extra
|
|
27
|
+
// properties are allowed outside fresh-literal checks), but the wire
|
|
28
|
+
// schema mirrors the server's validate_client: nested objects in the
|
|
29
|
+
// `client` bag reject the event at ingest.
|
|
30
|
+
const nestedClient = { os: "macos", screen: { width: 1512, height: 982 } };
|
|
31
|
+
const invalidTurn: TurnTelemetryEvent = {
|
|
32
|
+
...turnEventSample,
|
|
33
|
+
client: nestedClient,
|
|
34
|
+
};
|
|
35
|
+
const result = telemetryEventSchema.safeParse(invalidTurn);
|
|
36
|
+
expect(result.success).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
package/src/telemetry/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
2
2
|
import type { UsageAttributionProfileSource } from "../usage/types.js";
|
|
3
|
+
import type * as wire from "./telemetry-wire.generated.js";
|
|
4
|
+
import type { TurnOutcome } from "./turn-outcome.js";
|
|
3
5
|
|
|
4
6
|
/** Base fields present on every telemetry event. */
|
|
5
7
|
export interface TelemetryEventBase {
|
|
@@ -54,6 +56,18 @@ export interface ModelTelemetryEventBase extends TelemetryEventBase {
|
|
|
54
56
|
inference_profile_source: UsageAttributionProfileSource | null;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Applies the daemon's record-time guarantee to a wire-derived event type.
|
|
61
|
+
*
|
|
62
|
+
* The wire contract marks `assistant_version` optional for back-compat with
|
|
63
|
+
* old daemons; under the platform's per-event-wins contract a present
|
|
64
|
+
* per-event value (including explicit `null`) beats the envelope fallback.
|
|
65
|
+
* This daemon always stamps the field at record time (see
|
|
66
|
+
* {@link TelemetryEventBase.assistant_version} for the full contract), so
|
|
67
|
+
* wire-derived event types are re-typed with the field required.
|
|
68
|
+
*/
|
|
69
|
+
type Daemonize<T> = T & { assistant_version: string | null };
|
|
70
|
+
|
|
57
71
|
/**
|
|
58
72
|
* LLM usage event — one per persisted usage row. The main agent loop
|
|
59
73
|
* persists a single row per turn with token totals summed across every
|
|
@@ -329,7 +343,7 @@ export interface TurnTelemetryEvent extends TelemetryEventBase {
|
|
|
329
343
|
* could land — so `absent + no assistant message in trace` isolates the
|
|
330
344
|
* genuinely anomalous (crashed/unknown) turns.
|
|
331
345
|
*/
|
|
332
|
-
outcome?:
|
|
346
|
+
outcome?: TurnOutcome;
|
|
333
347
|
/**
|
|
334
348
|
* For `outcome: "batched"` turns: the `daemon_event_id` of the
|
|
335
349
|
* batch-final turn whose window carries the shared response. Omitted
|
|
@@ -358,64 +372,6 @@ export interface TurnTelemetryEvent extends TelemetryEventBase {
|
|
|
358
372
|
trace?: TurnTrace | null;
|
|
359
373
|
}
|
|
360
374
|
|
|
361
|
-
/** Lifecycle event — app_open, hatch, etc. */
|
|
362
|
-
export interface LifecycleTelemetryEvent extends TelemetryEventBase {
|
|
363
|
-
type: "lifecycle";
|
|
364
|
-
event_name: string;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/** Onboarding event — pre-chat selections and Google connect status. */
|
|
368
|
-
export interface OnboardingTelemetryEvent extends TelemetryEventBase {
|
|
369
|
-
type: "onboarding";
|
|
370
|
-
screen: string;
|
|
371
|
-
tools?: string[];
|
|
372
|
-
tasks?: string[];
|
|
373
|
-
tone?: string;
|
|
374
|
-
google_connected?: boolean;
|
|
375
|
-
google_scopes?: string[];
|
|
376
|
-
ab_variant?: string;
|
|
377
|
-
/**
|
|
378
|
-
* Activation-funnel fields (mirror the web funnel shape and the platform
|
|
379
|
-
* serializer). The platform accepts an onboarding event via either the
|
|
380
|
-
* legacy `screen` path or the all-funnel-fields path (`session_id` +
|
|
381
|
-
* `step_name` + `step_index` + `completed_at` + `funnel_version` +
|
|
382
|
-
* `ab_variant`).
|
|
383
|
-
*/
|
|
384
|
-
session_id?: string;
|
|
385
|
-
step_name?: string;
|
|
386
|
-
step_index?: number;
|
|
387
|
-
completed_at?: string;
|
|
388
|
-
funnel_version?: string;
|
|
389
|
-
user_id?: string;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Auth-fallback event — aggregated count of requests served via the legacy
|
|
394
|
-
* loopback auth fallback. One event per (guard, path, failure_kind) per flush
|
|
395
|
-
* window. Lets the platform see which deployments still rely on the loopback
|
|
396
|
-
* exemption instead of sending a bearer token.
|
|
397
|
-
*/
|
|
398
|
-
export interface AuthFallbackTelemetryEvent extends TelemetryEventBase {
|
|
399
|
-
type: "auth_fallback";
|
|
400
|
-
/** Which auth guard fell back: `"edge"` | `"edge-scoped"` | `"edge-guardian"`. */
|
|
401
|
-
guard: string;
|
|
402
|
-
/** Request pathname that fell back. */
|
|
403
|
-
path: string;
|
|
404
|
-
/**
|
|
405
|
-
* Why the bearer-token check did not succeed before the fallback:
|
|
406
|
-
* `"missing_authorization"` | `"malformed_authorization"` |
|
|
407
|
-
* `"token_validation_failed"` | `"insufficient_scope"` |
|
|
408
|
-
* `"non_actor_principal"` | `"guardian_mismatch"`.
|
|
409
|
-
*/
|
|
410
|
-
failure_kind: string;
|
|
411
|
-
/** Number of requests that fell back for this key during the window. */
|
|
412
|
-
count: number;
|
|
413
|
-
/** Window start (epoch ms) the count was accumulated over. */
|
|
414
|
-
window_start: number;
|
|
415
|
-
/** Window end (epoch ms) the count was accumulated over. */
|
|
416
|
-
window_end: number;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
375
|
/**
|
|
420
376
|
* Tool-executed event — one per tool invocation. Carries NO tool
|
|
421
377
|
* args/inputs or result contents (customer PII per ToS) — payload sizes
|
|
@@ -490,27 +446,6 @@ export interface WatchdogTelemetryEvent extends TelemetryEventBase {
|
|
|
490
446
|
detail: Record<string, unknown> | null;
|
|
491
447
|
}
|
|
492
448
|
|
|
493
|
-
/**
|
|
494
|
-
* Config-setting event — records a tracked config key's effective value.
|
|
495
|
-
* A single string:string pair on top of the standard envelope, mirroring
|
|
496
|
-
* the platform `ConfigSettingTelemetryEventSerializer`:
|
|
497
|
-
*
|
|
498
|
-
* - `config_key` — dotted config path (e.g. `"memory.enabled"`).
|
|
499
|
-
* Bounded server-side at 128 chars.
|
|
500
|
-
* - `config_value` — the effective value rendered as a string
|
|
501
|
-
* (`"true"` / `"false"` for booleans). Bounded server-side at 256
|
|
502
|
-
* chars.
|
|
503
|
-
*
|
|
504
|
-
* Metadata only — emitters record an explicit allowlist of non-sensitive
|
|
505
|
-
* settings, never free-form config content. Dedupe downstream on
|
|
506
|
-
* `daemon_event_id` (the daemon retries a batch on transient POST failure).
|
|
507
|
-
*/
|
|
508
|
-
export interface ConfigSettingTelemetryEvent extends TelemetryEventBase {
|
|
509
|
-
type: "config_setting";
|
|
510
|
-
config_key: string;
|
|
511
|
-
config_value: string;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
449
|
/** One inferred fact from the onboarding research-onboarding web-search turn. */
|
|
515
450
|
export interface OnboardingResearchClaim {
|
|
516
451
|
claim: string;
|
|
@@ -553,18 +488,175 @@ export interface OnboardingResearchTelemetryEvent extends TelemetryEventBase {
|
|
|
553
488
|
installed_plugins: string[];
|
|
554
489
|
}
|
|
555
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Wire-derived event types with the daemon's record-time
|
|
493
|
+
* `assistant_version` guarantee applied. New event types added to the wire
|
|
494
|
+
* contract flow through here with zero hand edits.
|
|
495
|
+
*/
|
|
496
|
+
type WireDaemonized = {
|
|
497
|
+
[K in keyof wire.WireEventMap]: Daemonize<wire.WireEventMap[K]>;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
/** Events where the daemon's type is intentionally narrower than the wire. */
|
|
501
|
+
type Overrides = {
|
|
502
|
+
llm_usage: LlmUsageTelemetryEvent;
|
|
503
|
+
turn: TurnTelemetryEvent;
|
|
504
|
+
tool_executed: ToolExecutedTelemetryEvent;
|
|
505
|
+
skill_loaded: SkillLoadedTelemetryEvent;
|
|
506
|
+
watchdog: WatchdogTelemetryEvent;
|
|
507
|
+
onboarding_research: OnboardingResearchTelemetryEvent;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Daemon-only event types not (yet) in the platform wire contract — i.e. types
|
|
512
|
+
* the daemon emits but `POST /v1/telemetry/ingest/` would silently skip because
|
|
513
|
+
* they have no serializer in the platform's `TELEMETRY_EVENT_SERIALIZERS`.
|
|
514
|
+
*
|
|
515
|
+
* Currently EMPTY: every emitted type is in the wire contract. When the daemon
|
|
516
|
+
* needs to emit a type before the platform accepts it, add it here (keyed by
|
|
517
|
+
* its wire `type`). Once the platform adds the serializer and the wire sync
|
|
518
|
+
* lands, the key must leave `Extensions` — into `Overrides` if the daemon type
|
|
519
|
+
* stays narrower than the wire, or plain wire flow-through otherwise. The
|
|
520
|
+
* `_extensionsDontCollide` guard below turns red until that move is made.
|
|
521
|
+
*/
|
|
522
|
+
type Extensions = Record<never, never>;
|
|
523
|
+
|
|
524
|
+
type EventMap = Omit<WireDaemonized, keyof Overrides> & Overrides & Extensions;
|
|
525
|
+
|
|
556
526
|
/** Discriminated union of all telemetry event types. */
|
|
557
|
-
export type TelemetryEvent =
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
527
|
+
export type TelemetryEvent = EventMap[keyof EventMap];
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Lifecycle event — app_open, hatch, etc. 1:1 with the wire contract:
|
|
531
|
+
* `telemetry-wire.generated.ts` (from the platform's
|
|
532
|
+
* `LifecycleTelemetryEventSerializer`) is the source of truth.
|
|
533
|
+
*/
|
|
534
|
+
export type LifecycleTelemetryEvent = EventMap["lifecycle"];
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Onboarding event — pre-chat selections, Google connect status, and
|
|
538
|
+
* activation-funnel steps. 1:1 with the wire contract
|
|
539
|
+
* (`OnboardingTelemetryEventSerializer`); the platform accepts either the
|
|
540
|
+
* legacy `screen` shape or the complete funnel step field set — see the wire
|
|
541
|
+
* schema's superRefine.
|
|
542
|
+
*/
|
|
543
|
+
export type OnboardingTelemetryEvent = EventMap["onboarding"];
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Auth-fallback event — aggregated count of requests served via the legacy
|
|
547
|
+
* loopback auth fallback, one event per (guard, path, failure_kind) per
|
|
548
|
+
* flush window (`count` is a per-window delta, not a running total). 1:1
|
|
549
|
+
* with the wire contract (`AuthFallbackTelemetryEventSerializer`).
|
|
550
|
+
*/
|
|
551
|
+
export type AuthFallbackTelemetryEvent = EventMap["auth_fallback"];
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Config-setting event — a tracked config key's effective value rendered as
|
|
555
|
+
* a string; emitters record an explicit allowlist of non-sensitive settings,
|
|
556
|
+
* never free-form config content. 1:1 with the wire contract
|
|
557
|
+
* (`ConfigSettingTelemetryEventSerializer`).
|
|
558
|
+
*/
|
|
559
|
+
export type ConfigSettingTelemetryEvent = EventMap["config_setting"];
|
|
560
|
+
|
|
561
|
+
// ---- Compile-time drift guards ----
|
|
562
|
+
// Each `Overrides` entry is pinned to its wire type in BOTH directions, so a
|
|
563
|
+
// wire sync PR that moves the platform contract turns red here instead of
|
|
564
|
+
// drifting silently:
|
|
565
|
+
//
|
|
566
|
+
// - `_*Narrows` — the daemon type stays assignable to the wire type, i.e.
|
|
567
|
+
// every daemon field's value is acceptable to the wire. Catches wire-side
|
|
568
|
+
// tightening (narrowed unions, newly required fields) and daemon-side
|
|
569
|
+
// loosening.
|
|
570
|
+
// - `_*KeysExist` — every key the daemon emits still exists on the wire
|
|
571
|
+
// type. Structural subtyping treats a daemon field the wire dropped as a
|
|
572
|
+
// harmless extra property, so `_*Narrows` alone stays green when the
|
|
573
|
+
// platform REMOVES or RENAMES a field; this key-set check is what catches
|
|
574
|
+
// those.
|
|
575
|
+
//
|
|
576
|
+
// Flow-through (non-override) events need no guards: they use the generated
|
|
577
|
+
// wire types directly, so their construction sites get excess-property /
|
|
578
|
+
// missing-field errors the moment the contract moves.
|
|
579
|
+
type AssertNarrows<_Narrow extends Wide, Wide> = true;
|
|
580
|
+
type AssertNoWireCollision<_Keys extends never> = true;
|
|
581
|
+
|
|
582
|
+
// `raw_usage` is excluded: the server treats it as an opaque JSONField, so
|
|
583
|
+
// the daemon's `Record<string, unknown>` (not structurally assignable to the
|
|
584
|
+
// wire's recursive `JsonValue`) is wire-safe — any JSON-serializable shape
|
|
585
|
+
// ships fine.
|
|
586
|
+
type _llmUsageNarrows = AssertNarrows<
|
|
587
|
+
Omit<LlmUsageTelemetryEvent, "raw_usage">,
|
|
588
|
+
Omit<wire.LlmUsageTelemetryEvent, "raw_usage">
|
|
589
|
+
>;
|
|
590
|
+
// `trace` and `client` are excluded: the server treats both as opaque JSON
|
|
591
|
+
// (JSONField/DictField), so the daemon's richer `TurnTrace`
|
|
592
|
+
// (`content: unknown`) and `TurnTelemetryClientInfo` shapes are wire-safe
|
|
593
|
+
// even though they are not assignable to `JsonValue`. Runtime bounds are
|
|
594
|
+
// enforced by the wire schema's superRefines, not by these types.
|
|
595
|
+
type _turnNarrows = AssertNarrows<
|
|
596
|
+
Omit<TurnTelemetryEvent, "trace" | "client">,
|
|
597
|
+
Omit<wire.TurnTelemetryEvent, "trace" | "client">
|
|
598
|
+
>;
|
|
599
|
+
type _toolExecutedNarrows = AssertNarrows<
|
|
600
|
+
ToolExecutedTelemetryEvent,
|
|
601
|
+
wire.ToolExecutedTelemetryEvent
|
|
602
|
+
>;
|
|
603
|
+
type _skillLoadedNarrows = AssertNarrows<
|
|
604
|
+
SkillLoadedTelemetryEvent,
|
|
605
|
+
wire.SkillLoadedTelemetryEvent
|
|
606
|
+
>;
|
|
607
|
+
// `detail` is excluded: the server treats it as an opaque JSONField, so the
|
|
608
|
+
// daemon's `Record<string, unknown>` bag is wire-safe; the serialized-size
|
|
609
|
+
// bound is enforced by the wire schema's superRefine.
|
|
610
|
+
type _watchdogNarrows = AssertNarrows<
|
|
611
|
+
Omit<WatchdogTelemetryEvent, "detail">,
|
|
612
|
+
Omit<wire.WatchdogTelemetryEvent, "detail">
|
|
613
|
+
>;
|
|
614
|
+
// Reverse (key-existence) direction. Unlike the `_*Narrows` guards, the
|
|
615
|
+
// opaque-JSON fields are NOT excluded here: opacity is about a field's
|
|
616
|
+
// SHAPE (the daemon's richer types aren't assignable to `JsonValue`), but
|
|
617
|
+
// key PRESENCE is part of the typed contract — the generated wire types
|
|
618
|
+
// declare `raw_usage`/`trace`/`client`/`detail`, and a platform sync that
|
|
619
|
+
// removed or renamed one of them would otherwise leave every guard green
|
|
620
|
+
// while the daemon kept emitting a field the server discards.
|
|
621
|
+
type _llmUsageKeysExist = AssertNarrows<
|
|
622
|
+
keyof LlmUsageTelemetryEvent,
|
|
623
|
+
keyof wire.LlmUsageTelemetryEvent
|
|
624
|
+
>;
|
|
625
|
+
type _turnKeysExist = AssertNarrows<
|
|
626
|
+
keyof TurnTelemetryEvent,
|
|
627
|
+
keyof wire.TurnTelemetryEvent
|
|
628
|
+
>;
|
|
629
|
+
type _toolExecutedKeysExist = AssertNarrows<
|
|
630
|
+
keyof ToolExecutedTelemetryEvent,
|
|
631
|
+
keyof wire.ToolExecutedTelemetryEvent
|
|
632
|
+
>;
|
|
633
|
+
type _skillLoadedKeysExist = AssertNarrows<
|
|
634
|
+
keyof SkillLoadedTelemetryEvent,
|
|
635
|
+
keyof wire.SkillLoadedTelemetryEvent
|
|
636
|
+
>;
|
|
637
|
+
type _watchdogKeysExist = AssertNarrows<
|
|
638
|
+
keyof WatchdogTelemetryEvent,
|
|
639
|
+
keyof wire.WatchdogTelemetryEvent
|
|
640
|
+
>;
|
|
641
|
+
// `claims` and `suggestions` are excluded from the narrows: the server types
|
|
642
|
+
// both as opaque JSON arrays (`z.array(jsonValueSchema)`), so the daemon's
|
|
643
|
+
// structured `OnboardingResearchClaim[]` / `OnboardingResearchSuggestion[]`
|
|
644
|
+
// shapes are wire-safe even though they aren't assignable to `JsonValue[]`.
|
|
645
|
+
// Runtime size bounds are enforced by the wire schema's superRefine.
|
|
646
|
+
type _onboardingResearchNarrows = AssertNarrows<
|
|
647
|
+
Omit<OnboardingResearchTelemetryEvent, "claims" | "suggestions">,
|
|
648
|
+
Omit<wire.OnboardingResearchTelemetryEvent, "claims" | "suggestions">
|
|
649
|
+
>;
|
|
650
|
+
type _onboardingResearchKeysExist = AssertNarrows<
|
|
651
|
+
keyof OnboardingResearchTelemetryEvent,
|
|
652
|
+
keyof wire.OnboardingResearchTelemetryEvent
|
|
653
|
+
>;
|
|
654
|
+
// An `Extensions` key that also exists in the wire map would silently
|
|
655
|
+
// shadow the generated type; this stays `never` only while the key sets
|
|
656
|
+
// are disjoint.
|
|
657
|
+
type _extensionsDontCollide = AssertNoWireCollision<
|
|
658
|
+
keyof Extensions & keyof wire.WireEventMap
|
|
659
|
+
>;
|
|
568
660
|
|
|
569
661
|
/**
|
|
570
662
|
* Event names backed by the `telemetry_events` outbox. Each name doubles as
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
watermarkKeysForSource,
|
|
47
47
|
} from "./telemetry-event-sources.js";
|
|
48
48
|
import { useReadOnlyMainDbForTelemetry } from "./telemetry-main-db.js";
|
|
49
|
+
import { validateWireEvents } from "./telemetry-wire-validation.js";
|
|
49
50
|
|
|
50
51
|
const log = getLogger("usage-telemetry");
|
|
51
52
|
|
|
@@ -405,6 +406,10 @@ export class UsageTelemetryReporter {
|
|
|
405
406
|
// in cursor order.
|
|
406
407
|
const typedEvents = batches.flatMap(({ batch }) => batch.events);
|
|
407
408
|
|
|
409
|
+
// Pre-flush wire validation — observability only: warns about events
|
|
410
|
+
// the server would silently drop; the batch is POSTed unchanged.
|
|
411
|
+
validateWireEvents(typedEvents, log);
|
|
412
|
+
|
|
408
413
|
const organizationId = getPlatformOrganizationId() || undefined;
|
|
409
414
|
const userId = getPlatformUserId() || undefined;
|
|
410
415
|
const payload = {
|
|
@@ -25,6 +25,7 @@ import { arePlatformFeaturesEnabled } from "../platform/feature-gate.js";
|
|
|
25
25
|
import { getDeviceId } from "../util/device-id.js";
|
|
26
26
|
import { getLogger } from "../util/logger.js";
|
|
27
27
|
import { APP_VERSION } from "../version.js";
|
|
28
|
+
import { validateWireEvents } from "./telemetry-wire-validation.js";
|
|
28
29
|
import type { WatchdogTelemetryEvent } from "./types.js";
|
|
29
30
|
|
|
30
31
|
const log = getLogger("watchdog-direct-emit");
|
|
@@ -62,6 +63,11 @@ export async function emitWatchdogEventDirect(
|
|
|
62
63
|
detail,
|
|
63
64
|
assistant_version: APP_VERSION,
|
|
64
65
|
};
|
|
66
|
+
|
|
67
|
+
// Pre-flush wire validation — observability only: warns when the server
|
|
68
|
+
// would silently drop the event; the POST proceeds unchanged.
|
|
69
|
+
validateWireEvents([event], log);
|
|
70
|
+
|
|
65
71
|
const organizationId = getPlatformOrganizationId() || undefined;
|
|
66
72
|
const userId = getPlatformUserId() || undefined;
|
|
67
73
|
const resp = await client.fetch(TELEMETRY_INGEST_PATH, {
|