Skip to content

Commit fede525

Browse files
TheLarkInnCopilot
andcommitted
Add allowlisted telemetry projection subscriber
Add the telemetry subscriber for @rushstack/reporter (#5858). - Add TelemetrySubscriber, which consumes canonical events before reporter filtering and produces an allowlisted aggregate at command completion - Keep only allowlisted values: command and result, timing, operation status counts, diagnostic codes and categories, reporter mode, and protocol and producer versions - Exclude messages, paths, raw output, command arguments, remediation parameters, stacks, and local-sensitive and secret values by construction - Add createTelemetryReporter to wire the subscriber into the manager - Preserve the legacy beforeLog hook through createBeforeLogAdapter - Add allowlist schema and leakage tests Assistant-model: GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 897dcf7e-e6e8-4a84-85ca-34b93fa29be3
1 parent bc54307 commit fede525

8 files changed

Lines changed: 573 additions & 1 deletion

File tree

common/reviews/api/reporter.api.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export type BootstrapPrivacyClassification = 'public' | 'local-sensitive' | 'sec
3838
// @beta
3939
export function computeEnvelopePrivacyFloor(classifications: Iterable<ReporterPrivacyClassification>): ReporterPrivacyClassification;
4040

41+
// @beta
42+
export function createBeforeLogAdapter(hooks: readonly LegacyBeforeLogHook[]): (aggregate: ITelemetryAggregate) => void;
43+
4144
// @beta
4245
export function createEngineSink(providedSink?: IReporterEventSink): IEngineSinkResolution;
4346

@@ -53,6 +56,9 @@ export function createScopedLogger(reporter: IScopedReporter): IScopedLogger;
5356
// @beta
5457
export function createScopedReporter(options: ICreateScopedReporterOptions): IScopedReporter;
5558

59+
// @beta
60+
export function createTelemetryReporter(subscriber: TelemetrySubscriber): IReporter;
61+
5662
// @beta
5763
export const DEFAULT_FLUSH_TIMEOUT_MS: number;
5864

@@ -480,6 +486,24 @@ export function isReporterProtocolCompatible(consumer: IReporterProtocolVersion,
480486
// @beta
481487
export function isValidRushDiagnosticCode(code: string): boolean;
482488

489+
// @beta
490+
export interface ITelemetryAggregate {
491+
readonly commandName?: string;
492+
readonly diagnosticCategoryCounts: {
493+
readonly [category: string]: number;
494+
};
495+
readonly diagnosticCodes: readonly string[];
496+
readonly durationMs?: number;
497+
readonly exitCode?: number;
498+
readonly operationStatusCounts: {
499+
readonly [status: string]: number;
500+
};
501+
readonly producerVersions: readonly string[];
502+
readonly protocolVersion?: IReporterProtocolVersion;
503+
readonly reporterMode?: string;
504+
readonly result?: TelemetryResult;
505+
}
506+
483507
// @beta
484508
export interface IWatchCycleCompletedPayload {
485509
readonly changedProjects?: readonly string[];
@@ -492,6 +516,9 @@ export interface IWriteBootstrapHandoffOptions {
492516
readonly pid?: number;
493517
}
494518

519+
// @beta
520+
export type LegacyBeforeLogHook = (telemetry: Record<string, unknown>) => void;
521+
495522
// @beta
496523
export class LegacyFallbackSink implements IReporterEventSink {
497524
// (undocumented)
@@ -672,6 +699,20 @@ export class RushSessionReporting {
672699
// @beta
673700
export function summarizeShadowResult(events: readonly IReporterEventEnvelope<unknown>[]): IShadowResultSummary;
674701

702+
// @beta
703+
export const TELEMETRY_AGGREGATE_KEYS: readonly string[];
704+
705+
// @beta
706+
export type TelemetryResult = 'succeeded' | 'failed';
707+
708+
// @beta
709+
export class TelemetrySubscriber {
710+
constructor();
711+
buildAggregate(): ITelemetryAggregate;
712+
ingest(event: IReporterEventEnvelope<unknown>): void;
713+
setReporterMode(reporterMode: string): void;
714+
}
715+
675716
// @beta
676717
export function writeBootstrapHandoffFileAsync(buffer: BootstrapEventBuffer, options?: IWriteBootstrapHandoffOptions): Promise<string>;
677718

libraries/reporter/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ export { LifecycleEmitter } from './lifecycle/LifecycleEmitter';
143143
export type { IShadowResultSummary } from './lifecycle/ShadowParity';
144144
export { deriveExitCodeFromEvents, summarizeShadowResult } from './lifecycle/ShadowParity';
145145

146+
export type { TelemetryResult, ITelemetryAggregate } from './telemetry/TelemetryAggregate';
147+
export { TELEMETRY_AGGREGATE_KEYS } from './telemetry/TelemetryAggregate';
148+
export { TelemetrySubscriber, createTelemetryReporter } from './telemetry/TelemetrySubscriber';
149+
export type { LegacyBeforeLogHook } from './telemetry/BeforeLogAdapter';
150+
export { createBeforeLogAdapter } from './telemetry/BeforeLogAdapter';
151+
146152
export type { IReporterEmitEventInput, IReporterEventSink } from './producers/IReporterEventSink';
147153
export type {
148154
ReporterMessageSeverity,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import type { ITelemetryAggregate } from './TelemetryAggregate';
5+
6+
/**
7+
* The legacy `beforeLog` telemetry hook signature.
8+
*
9+
* @remarks
10+
* Existing telemetry consumers register a `beforeLog` hook that runs with the
11+
* telemetry record before it is written. The hook receives a plain object.
12+
*
13+
* @beta
14+
*/
15+
export type LegacyBeforeLogHook = (telemetry: Record<string, unknown>) => void;
16+
17+
/**
18+
* Adapts the allowlisted telemetry aggregate to the legacy `beforeLog` hook.
19+
*
20+
* @remarks
21+
* During migration the existing `beforeLog` hook is preserved: the adapter runs
22+
* each legacy hook with a plain-object copy of the new aggregate, so no hook
23+
* observes non-allowlisted data.
24+
*
25+
* @param hooks - the legacy hooks to preserve
26+
*
27+
* @beta
28+
*/
29+
export function createBeforeLogAdapter(
30+
hooks: readonly LegacyBeforeLogHook[]
31+
): (aggregate: ITelemetryAggregate) => void {
32+
return (aggregate: ITelemetryAggregate): void => {
33+
const record: Record<string, unknown> = { ...aggregate };
34+
for (const hook of hooks) {
35+
hook(record);
36+
}
37+
};
38+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import type { IReporterProtocolVersion } from '../events/ReporterProtocolVersion';
5+
6+
/**
7+
* The command result recorded by telemetry.
8+
*
9+
* @beta
10+
*/
11+
export type TelemetryResult = 'succeeded' | 'failed';
12+
13+
/**
14+
* The allowlisted telemetry aggregate produced at command completion.
15+
*
16+
* @remarks
17+
* Only these fields ever leave the machine. Messages, templates, paths, raw
18+
* stdout/stderr, command arguments, remediation parameters, stack traces, and
19+
* any local-sensitive or secret values are excluded by construction.
20+
*
21+
* @beta
22+
*/
23+
export interface ITelemetryAggregate {
24+
/**
25+
* The command name.
26+
*/
27+
readonly commandName?: string;
28+
29+
/**
30+
* Whether the command succeeded or failed.
31+
*/
32+
readonly result?: TelemetryResult;
33+
34+
/**
35+
* The process exit code.
36+
*/
37+
readonly exitCode?: number;
38+
39+
/**
40+
* The command or session duration in milliseconds.
41+
*/
42+
readonly durationMs?: number;
43+
44+
/**
45+
* The number of operations that reached each status.
46+
*/
47+
readonly operationStatusCounts: { readonly [status: string]: number };
48+
49+
/**
50+
* The distinct diagnostic codes emitted, sorted.
51+
*/
52+
readonly diagnosticCodes: readonly string[];
53+
54+
/**
55+
* The number of diagnostics emitted in each category.
56+
*/
57+
readonly diagnosticCategoryCounts: { readonly [category: string]: number };
58+
59+
/**
60+
* The selected reporter mode.
61+
*/
62+
readonly reporterMode?: string;
63+
64+
/**
65+
* The reporter protocol version.
66+
*/
67+
readonly protocolVersion?: IReporterProtocolVersion;
68+
69+
/**
70+
* The distinct `packageName@packageVersion` producers observed, sorted.
71+
*/
72+
readonly producerVersions: readonly string[];
73+
}
74+
75+
/**
76+
* The complete set of allowlisted telemetry aggregate keys.
77+
*
78+
* @remarks
79+
* Used to assert that no non-allowlisted field ever appears in the aggregate.
80+
*
81+
* @beta
82+
*/
83+
export const TELEMETRY_AGGREGATE_KEYS: readonly string[] = [
84+
'commandName',
85+
'result',
86+
'exitCode',
87+
'durationMs',
88+
'operationStatusCounts',
89+
'diagnosticCodes',
90+
'diagnosticCategoryCounts',
91+
'reporterMode',
92+
'protocolVersion',
93+
'producerVersions'
94+
];

0 commit comments

Comments
 (0)