mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
perf(mcp): lean event projection only on unfiltered reads; filtered reads stay complete
30-day tool telemetry: callers pass fields explicitly in more than 90 % of get_session_events / query_raw_events calls, and reads filtered by eventType keep the payload about half the time (the ime-decompile skill reads data.msiDownloadUrl after an eventType filter without fields). So the lean default (no data / no DataJson) now applies only to unfiltered timeline reads and announces itself in-band (omittedFields + omittedNote); a read filtered by eventType/severity/source returns full events as before. Never worse than the former default. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+1
-1
Submodule docs updated: 11fc393854...63dc1699e1
@@ -78,6 +78,32 @@ describe('first-page defaults', () => {
|
||||
expect(String(body.omittedNote)).toContain('data.<key>');
|
||||
});
|
||||
|
||||
it('a FILTERED get_session_events read stays complete: no projection, no omission marker', async () => {
|
||||
// Usage telemetry: filtered reads target specific events and want their payload about half the
|
||||
// time (and the ime-decompile skill reads data.msiDownloadUrl after an eventType filter).
|
||||
const handler = handlerFor('get_session_events');
|
||||
const { urls } = stubFetchCapture();
|
||||
|
||||
const result = await runWithCaller(GA, () => handler({ sessionId: SESSION, eventType: 'ime_agent_version' }, extra));
|
||||
|
||||
expect(urls).toHaveLength(1);
|
||||
expect(urls[0]).not.toContain('fields=');
|
||||
expect(urls[0]).toContain('eventType=ime_agent_version');
|
||||
expect(urls[0]).toContain(`pageSize=${DEFAULT_FIRST_PAGE_SIZE}`);
|
||||
expect(resultJson(result)).not.toHaveProperty('omittedFields');
|
||||
});
|
||||
|
||||
it('a FILTERED query_raw_events read stays complete: no projection, no omission marker', async () => {
|
||||
const handler = handlerFor('query_raw_events');
|
||||
const { urls } = stubFetchCapture();
|
||||
|
||||
const result = await runWithCaller(GA, () => handler({ sessionId: SESSION, source: 'RealmJoin' }, extra));
|
||||
|
||||
expect(urls).toHaveLength(1);
|
||||
expect(urls[0]).not.toContain('fields=');
|
||||
expect(resultJson(result)).not.toHaveProperty('omittedFields');
|
||||
});
|
||||
|
||||
it('get_session_events forwards an explicit projection verbatim (payload on request) without the omission marker', async () => {
|
||||
const handler = handlerFor('get_session_events');
|
||||
const { urls } = stubFetchCapture();
|
||||
|
||||
@@ -1106,7 +1106,7 @@ export function registerAdminTools(server: McpServer, ga: boolean, strictGa: boo
|
||||
startedAfter: z.string().optional().describe('ISO 8601 datetime — only events after this'),
|
||||
startedBefore: z.string().optional().describe('ISO 8601 datetime — only events before this'),
|
||||
fields: z.string().optional()
|
||||
.describe('Comma-separated pass-through projection over the literal stored column names (case-insensitive); narrows the row but never drops a real column. PartitionKey + RowKey are always kept. Stored columns: PartitionKey, RowKey, Timestamp (storage write time), OccurredUtc (event time), EventId, SessionId, TenantId, EventType, Severity (int), Source, Phase (int), Message, Sequence, DataJson (raw string), ReceivedAt, SentAt, OriginalTimestamp, TimestampClamped, CausedByTransitionStepIndex, CausedBySignalOrdinal. Omitted = every column EXCEPT DataJson (the multi-KB payload); list DataJson explicitly when you need it.'),
|
||||
.describe('Comma-separated pass-through projection over the literal stored column names (case-insensitive); narrows the row but never drops a real column. PartitionKey + RowKey are always kept. Stored columns: PartitionKey, RowKey, Timestamp (storage write time), OccurredUtc (event time), EventId, SessionId, TenantId, EventType, Severity (int), Source, Phase (int), Message, Sequence, DataJson (raw string), ReceivedAt, SentAt, OriginalTimestamp, TimestampClamped, CausedByTransitionStepIndex, CausedBySignalOrdinal. Omitted on an UNFILTERED read (no eventType/severity/source) = every column EXCEPT DataJson (the multi-KB payload), and the response says so (omittedFields); omitted on a FILTERED read = the full raw row including DataJson. List DataJson explicitly to force it either way.'),
|
||||
pageSize: z.coerce.number().int().min(1).max(1000).optional()
|
||||
.describe('Page size (1-1000; default ' + DEFAULT_FIRST_PAGE_SIZE + ' on the first page). Index rows walked per call; follow nextLink for more. On a follow-up call an explicit value overrides the pageSize embedded in the nextLink (the cursor stays valid) — omit it to keep the size the nextLink carries.'),
|
||||
continuation: z.string().optional()
|
||||
@@ -1118,10 +1118,14 @@ export function registerAdminTools(server: McpServer, ga: boolean, strictGa: boo
|
||||
try {
|
||||
const { tenantId: rawTenantId, sessionId, eventType, severity, source, startedAfter, startedBefore, fields: explicitFields, continuation } = args;
|
||||
const pageSize = pageSizeForCall(args.pageSize, continuation, DEFAULT_FIRST_PAGE_SIZE);
|
||||
// Lean by default: DataJson rides along only when listed. On a follow-up call an omitted
|
||||
// fields keeps whatever projection the nextLink carries (same rule as pageSize).
|
||||
const leanDefaultApplied = explicitFields === undefined && !continuation;
|
||||
const fields = explicitFields ?? (continuation ? undefined : LEAN_RAW_EVENT_FIELDS);
|
||||
// Default projection follows intent (see get_session_events): an UNFILTERED read (a
|
||||
// session's whole raw stream, or a bare cross-tenant walk) leaves DataJson out; a read
|
||||
// filtered by eventType/severity/source targets specific events and stays complete.
|
||||
// Explicit fields win either way; on a follow-up call an omitted fields keeps whatever
|
||||
// projection the nextLink carries (same rule as pageSize).
|
||||
const targeted = Boolean(eventType || severity || source);
|
||||
const leanDefaultApplied = explicitFields === undefined && !continuation && !targeted;
|
||||
const fields = explicitFields ?? (leanDefaultApplied ? LEAN_RAW_EVENT_FIELDS : undefined);
|
||||
const tenantId = enforceDelegatedTenantForPage(rawTenantId, continuation);
|
||||
if (eventType) assertKnownEventType(eventType);
|
||||
const basePath = pickGlobalOrTenantPath('/api/global/raw/events', '/api/raw/events', tenantId);
|
||||
|
||||
@@ -423,7 +423,7 @@ export function registerSessionTools(server: McpServer, ga: boolean, delegated:
|
||||
severity: z.enum(EVENT_SEVERITIES).optional(),
|
||||
source: z.string().optional().describe('Filter by event source/app name (e.g. "MicrosoftTeams")'),
|
||||
fields: z.string().optional()
|
||||
.describe('Comma-separated projection. Omitted = lean default "' + LEAN_EVENT_FIELDS + '" — the multi-KB "data" payload is NOT included. List "data" for the whole payload, or "data.<key>" entries (e.g. "data.errorCode,data.scriptType") for just those payload keys. Valid keys: eventId, sessionId, tenantId, eventType, severity, source, phase, phaseName, timestamp, receivedAt, sentAt, message, sequence, rowKey, originalTimestamp, timestampClamped, causedByTransitionStepIndex, causedBySignalOrdinal, data, data.<key>.'),
|
||||
.describe('Comma-separated projection. Omitted on an UNFILTERED read (no eventType/severity/source) = lean timeline default "' + LEAN_EVENT_FIELDS + '" — the multi-KB "data" payload is NOT included and the response says so (omittedFields). Omitted on a FILTERED read = full events including "data". List "data" for the whole payload, or "data.<key>" entries (e.g. "data.errorCode,data.scriptType") for just those payload keys. Valid keys: eventId, sessionId, tenantId, eventType, severity, source, phase, phaseName, timestamp, receivedAt, sentAt, message, sequence, rowKey, originalTimestamp, timestampClamped, causedByTransitionStepIndex, causedBySignalOrdinal, data, data.<key>.'),
|
||||
pageSize: z.coerce.number().int().min(1).max(1000).optional()
|
||||
.describe('Page size (1-1000; default ' + DEFAULT_FIRST_PAGE_SIZE + ' on the first page). Returns this many events per call; follow nextLink for more. On a follow-up call an explicit value overrides the pageSize embedded in the nextLink (the cursor stays valid); omit it to keep the size the nextLink carries.'),
|
||||
continuation: z.string().optional()
|
||||
@@ -435,10 +435,14 @@ export function registerSessionTools(server: McpServer, ga: boolean, delegated:
|
||||
try {
|
||||
const { sessionId, tenantId: rawTenantId, continuation, eventType, severity, source, fields: explicitFields } = args;
|
||||
const pageSize = pageSizeForCall(args.pageSize, continuation, DEFAULT_FIRST_PAGE_SIZE);
|
||||
// Lean by default: the payload rides along only when asked for. On a follow-up call an
|
||||
// Default projection follows intent (30-day usage telemetry, 2026-09-02): an UNFILTERED read
|
||||
// is a timeline skim — message-level, so the payload is left out; a read filtered by
|
||||
// eventType/severity/source targets specific events and wants their payload about half the
|
||||
// time, so it stays complete. Explicit fields win either way, and on a follow-up call an
|
||||
// omitted fields keeps whatever projection the nextLink carries (same rule as pageSize).
|
||||
const leanDefaultApplied = explicitFields === undefined && !continuation;
|
||||
const fields = explicitFields ?? (continuation ? undefined : LEAN_EVENT_FIELDS);
|
||||
const targeted = Boolean(eventType || severity || source);
|
||||
const leanDefaultApplied = explicitFields === undefined && !continuation && !targeted;
|
||||
const fields = explicitFields ?? (leanDefaultApplied ? LEAN_EVENT_FIELDS : undefined);
|
||||
const tenantId = enforceDelegatedTenantForPage(rawTenantId, continuation);
|
||||
if (eventType) assertKnownEventType(eventType);
|
||||
const basePath = `/api/sessions/${sessionId}/events`;
|
||||
|
||||
@@ -22,21 +22,28 @@ export const LEAN_RAW_EVENT_FIELDS =
|
||||
* In-band marker merged into a lean-default first page: a caller reading the RESULT (not the
|
||||
* schema) learns what was left out and the exact argument that brings it back. The model
|
||||
* must never have to guess whether a payload exists — omission is announced, not silent.
|
||||
*
|
||||
* The lean default applies to UNFILTERED reads only (a timeline skim). Usage telemetry
|
||||
* (30 days, 2026-09-02) shows callers project explicitly in >90 % of calls and, when they
|
||||
* filter by eventType, keep the payload about half the time — so a filtered read stays
|
||||
* complete and this marker never appears on it.
|
||||
*/
|
||||
export const LEAN_EVENT_OMISSION = {
|
||||
omittedFields: ['data'],
|
||||
omittedNote:
|
||||
'The per-event payload ("data", up to tens of KB per event) is omitted by default. ' +
|
||||
'The per-event payload ("data", up to tens of KB per event) is omitted on an unfiltered timeline read. ' +
|
||||
`Re-run with fields="${LEAN_EVENT_FIELDS},data" for the whole payload, or add "data.<key>" entries ` +
|
||||
'(e.g. "data.errorCode") for just those keys; a nextLink keeps whatever projection you chose.',
|
||||
'(e.g. "data.errorCode") for just those keys; a read filtered by eventType/severity/source includes ' +
|
||||
'the payload by default, and a nextLink keeps whatever projection you chose.',
|
||||
} as const;
|
||||
|
||||
/** Raw-events counterpart of LEAN_EVENT_OMISSION. */
|
||||
export const LEAN_RAW_EVENT_OMISSION = {
|
||||
omittedFields: ['DataJson'],
|
||||
omittedNote:
|
||||
'DataJson (the raw per-event payload string) is omitted by default. ' +
|
||||
`Re-run with fields="${LEAN_RAW_EVENT_FIELDS},DataJson" to include it; a nextLink keeps whatever projection you chose.`,
|
||||
'DataJson (the raw per-event payload string) is omitted on an unfiltered read. ' +
|
||||
`Re-run with fields="${LEAN_RAW_EVENT_FIELDS},DataJson" to include it; a read filtered by ` +
|
||||
'eventType/severity/source includes it by default, and a nextLink keeps whatever projection you chose.',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user