src/vs/platform/agentHost/node/agentHostRestrictedTelemetry.ts

276 LOC · 256 covered · 20 uncovered · 39 ranges · 2008 concepts · 9 introducers · 937 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentHostRestrictedTelemetry.ts ×13
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { generateUuid } from '../../../base/common/uuid.js';
7 > import { ILogService } from '../../log/common/log.js';
8 > import { ICommonProperties } from '../../telemetry/common/telemetry.js';
9 >
10 > /**
11 > * Public GitHub Copilot telemetry ingestion keys. These are instrumentation keys, not
12 > * secrets; the iKey selects the destination hydro table:
13 > * - standard -> `copilot_v0_copilot_event`
14 > * - enhanced -> `copilot_v0_restricted_copilot_event`
15 > */
16 > const GH_STANDARD_IKEY = '7d7048df-6dd0-4048-bb23-b716c1461f8f';
17 > const GH_ENHANCED_IKEY = '3fdd7f28-937a-48c8-9a21-ba337db23bd1';
18 >
19 > /**
20 > * Fallback Copilot telemetry endpoint (the dotcom value of the CAPI token's
21 > * `endpoints.telemetry`, with the `/telemetry` path the Copilot CLI/runtime appends).
22 > * Used until {@link IAgentHostRestrictedTelemetry.setRestrictedTelemetryEndpoint} supplies
23 > * the user's discovered endpoint (dotcom, GHE, or proxy). Accepts unauthenticated POSTs.
24 > */
25 > const GH_TELEMETRY_URL = 'https://copilot-telemetry.githubusercontent.com/telemetry';
26 >
27 > /** Event names are namespaced by client category; the CTS name filter requires this. */
28 > const NAMESPACE = 'copilot-chat';
29 >
30 > export type TelemetryProps = Record<string, string | undefined>;
31 > export type TelemetryMeasurements = Record<string, number | undefined>;
32 >
33 > export interface IAgentHostInternalTelemetryContext {
34 > readonly isInternal: boolean;
35 > readonly trackingId: string | undefined;
36 > readonly userName: string | undefined;
37 > readonly isVscodeTeamMember: boolean;
38 > }
39 >
40 > export interface IAgentHostRestrictedTelemetryContext extends IAgentHostInternalTelemetryContext {
41 > readonly restrictedTelemetryEnabled: boolean;
42 > readonly telemetryEndpoint: string | undefined;
43 > /** Whether content exclusion is enabled; undefined when account discovery could not determine it. */
44 > readonly copilotIgnoreEnabled?: boolean;
45 > }
46 >
47 > export interface IAgentHostInternalTelemetrySink {
48 > setContext(context: IAgentHostInternalTelemetryContext | undefined): void;
49 > send(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
50 > sendForContext(context: IAgentHostInternalTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
51 > }
52 >
53 > /** The subset of the global `fetch` used to POST envelopes; injectable so tests avoid live network calls. */
54 > export type FetchFn = typeof globalThis.fetch;
55 >
56 > /**
57 > * App Insights caps a single property value at ~8192 chars. Long values are split across
58 > * numbered keys (`key`, `key_02`, `key_03`, …) so the Copilot Telemetry Service reassembles
59 > * them, mirroring the Copilot extension's `multiplexProperties` so events look identical on the
60 > * wire and downstream.
61 > */
62 > const MAX_PROPERTY_LENGTH = 8192;
63 > const MAX_CONCATENATED_PROPERTIES = 50;
64 >
65 > export function multiplexProperties(properties: TelemetryProps): TelemetryProps {
66 > const newProperties: TelemetryProps = { ...properties }; agentHostRestrictedTelemetry.ts ×2
67 > for (const key in properties) {
68 > const value = properties[key]; agentHostRestrictedTelemetry.ts ×2
69 > let remaining = value?.length ?? 0;
70 > if (remaining > MAX_PROPERTY_LENGTH) {
71 > let lastStartIndex = 0; agentHostRestrictedTelemetry.ts ×1
72 > let count = 0;
73 > while (remaining > 0 && count < MAX_CONCATENATED_PROPERTIES) {
74 > count += 1;
75 > let propertyName = key;
76 > if (count > 1) {
77 > propertyName = key + '_' + (count < 10 ? '0' : '') + count;
78 > }
79 > let offsetIndex = lastStartIndex + MAX_PROPERTY_LENGTH;
80 > if (remaining < MAX_PROPERTY_LENGTH) {
81 > offsetIndex = lastStartIndex + remaining;
82 > }
83 > newProperties[propertyName] = value!.slice(lastStartIndex, offsetIndex);
84 > remaining -= MAX_PROPERTY_LENGTH;
85 > lastStartIndex += MAX_PROPERTY_LENGTH;
86 > }
87 > }
89 > return newProperties; agentHostRestrictedTelemetry.ts ×2
90 > }
92 > /**
93 > * The restricted telemetry surface the agent host exposes, mirroring the Copilot extension's
94 > * `ITelemetryService` restricted methods so agent-host code can emit the same GH/MSFT events.
95 > */
96 > export interface IAgentHostRestrictedTelemetry {
97 > /** GH standard (non-restricted) telemetry -> `copilot_v0_copilot_event`. */
98 > sendGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
99 > /** GH enhanced/restricted telemetry (prompts, tools, etc.) -> `copilot_v0_restricted_copilot_event`. */
100 > sendEnhancedGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
101 > /** GH enhanced telemetry attributed and routed using an immutable per-session context. */
102 > sendEnhancedGHTelemetryEventForContext(context: IAgentHostRestrictedTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
103 > /** MSFT-internal telemetry -> Aria/Collector++ (internal-only table). No-op without an internal key. */
104 > sendInternalMSFTTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
105 > /** MSFT-internal telemetry attributed using an immutable per-session context. */
106 > sendInternalMSFTTelemetryEventForContext(context: IAgentHostInternalTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void;
107 > /** Sets the Copilot user tracking id (`copilot_trackingId`) carried on every subsequent event. */
108 > setCopilotTrackingId(trackingId: string | undefined): void;
109 > /** Overrides the POST endpoint with the user's CAPI `endpoints.telemetry`; falsy restores the default. */
110 > setRestrictedTelemetryEndpoint(endpointUrl: string | undefined): void;
111 > /** Enables enhanced GH telemetry once the token opts in (`rt=1`); off by default and on flip/logout. */
112 > setRestrictedTelemetryEnabled(enabled: boolean): void;
113 > /** Sets the internal-user identity and enables the internal sink only for staff accounts. */
114 > setInternalTelemetryContext(context: IAgentHostInternalTelemetryContext | undefined): void;
115 > }
116 >
117 > /**
118 > * Emits GitHub Copilot restricted/enhanced telemetry from the agent-host process by POSTing
119 > * Application-Insights envelopes to the Copilot telemetry endpoint (the same wire format the
120 > * Copilot extension uses). Fire-and-forget; failures are logged, never thrown.
121 > */
122 > export class AgentHostRestrictedTelemetrySender implements IAgentHostRestrictedTelemetry {
123 >
124 > private readonly _commonProps: TelemetryProps;
125 >
126 > /**
127 > * Whether the current Copilot token opts into enhanced/restricted telemetry (`rt=1`). Off by
128 > * default so the sole writer to the restricted table never emits for public users — a hard
129 > * safety boundary that holds even if the enclosing service's gate is bypassed. Mirrors the
130 > * Copilot extension, which only creates the restricted reporter for opted-in users.
131 > */
132 > private _restrictedTelemetryEnabled = false;
133 > private _internalTelemetryEnabled = false;
134 >
135 > constructor(
136 > commonProperties: ICommonProperties, agentHostRestrictedTelemetry.ts ×2
137 > private readonly _logService: ILogService,
138 > private _endpointUrl: string = GH_TELEMETRY_URL,
139 > private readonly _internalSink?: IAgentHostInternalTelemetrySink,
140 > private readonly _fetchFn: FetchFn = globalThis.fetch,
141 > ) {
142 > // Map the resolved common properties onto the GH property names the hydro schema reads.
143 > this._commonProps = {
144 > client_machineid: asString(commonProperties['common.machineId']),
145 > client_deviceid: asString(commonProperties['common.devDeviceId']),
146 > client_sessionid: asString(commonProperties['sessionID']),
147 > common_os: asString(commonProperties['common.nodePlatform']) ?? process.platform,
148 > editor_version: asString(commonProperties['version']),
149 > };
150 > }
152 > sendGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
153 this._post(GH_STANDARD_IKEY, eventName, properties, measurements);
154 }
156 > sendEnhancedGHTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
157 > // Hard safety boundary: enhanced/restricted telemetry is the pipeline that may carry prompt agentHostRestrictedTelemetry.ts ×3
158 > // and tool content, so the only writer to the restricted table refuses to emit unless the
159 > // user's token opted in (`rt=1`). This holds even if a caller reaches the sender without the
160 > // service-level `rt`/telemetry-level gate.
161 > if (!this._restrictedTelemetryEnabled) {
162 > return;
163 > }
164 > this._post(GH_ENHANCED_IKEY, eventName, properties, measurements);
165 > }
167 > sendEnhancedGHTelemetryEventForContext(context: IAgentHostRestrictedTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
168 > if (!context.restrictedTelemetryEnabled) { agentHostRestrictedTelemetry.ts ×5
169 return;
170 }
171 > this._post(GH_ENHANCED_IKEY, eventName, properties, measurements, { agentHostRestrictedTelemetry.ts ×5
172 > endpointUrl: context.telemetryEndpoint,
173 > trackingId: context.trackingId,
174 > });
175 > }
177 > sendInternalMSFTTelemetryEvent(eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
178 > if (!this._internalTelemetryEnabled) { agentHostRestrictedTelemetry.ts ×3
179 > return;
180 > }
181 > if (this._internalSink) {
182 > this._internalSink.send(eventName, properties, measurements);
183 > return;
184 > }
185 this._logService.trace(`[ahp-restricted] internal MSFT event (not sent, no internal key): ${eventName}`);
188 > sendInternalMSFTTelemetryEventForContext(context: IAgentHostInternalTelemetryContext, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements): void {
189 if (!context.isInternal) {
190 return;
191 }
192 if (this._internalSink) {
193 this._internalSink.sendForContext(context, eventName, properties, measurements);
194 return;
195 }
196 this._logService.trace(`[ahp-restricted] internal MSFT event (not sent, no internal key): ${eventName}`);
197 }
199 > setCopilotTrackingId(trackingId: string | undefined): void {
200 > // `copilot_trackingId` is the current account's Copilot token `tid` claim. Exact runtime agentHostRestrictedTelemetry.ts ×5
201 > // targets use their immutable per-session context instead; this mutable value remains for
202 > // the pre-existing account-scoped reporters.
203 > this._commonProps.copilot_trackingId = trackingId || undefined;
204 > }
206 > setRestrictedTelemetryEndpoint(endpointUrl: string | undefined): void {
207 > // The user's telemetry host comes from the CAPI `endpoints.telemetry` discovery; fall back agentHostRestrictedTelemetry.ts ×8
208 > // to the dotcom default when it is unknown so events are never sent to an empty URL.
209 > this._endpointUrl = endpointUrl || GH_TELEMETRY_URL;
210 > }
212 > setRestrictedTelemetryEnabled(enabled: boolean): void {
213 > this._restrictedTelemetryEnabled = enabled; agentHostRestrictedTelemetry.ts ×8
214 > }
216 > setInternalTelemetryContext(context: IAgentHostInternalTelemetryContext | undefined): void {
217 > this._internalTelemetryEnabled = context?.isInternal === true; agentHostRestrictedTelemetry.ts ×3
218 > this._internalSink?.setContext(context);
219 > }
221 > private _post(iKey: string, eventName: string, properties?: TelemetryProps, measurements?: TelemetryMeasurements, context?: { readonly endpointUrl: string | undefined; readonly trackingId: string | undefined }): void {
222 > const name = eventName.includes('/') ? eventName : `${NAMESPACE}/${eventName}`; agentHostRestrictedTelemetry.ts ×8
223 > const commonProps = context
224 > ? { ...this._commonProps, copilot_trackingId: context.trackingId } agentHostRestrictedTelemetry.ts ×5
225 > : this._commonProps; agentHostRestrictedTelemetry.ts ×3
226 > const envelope = { agentHostRestrictedTelemetry.ts ×8
227 > ver: 1,
228 > name: `Microsoft.ApplicationInsights.${iKey.replace(/-/g, '')}.Event`,
229 > time: new Date().toISOString(),
230 > sampleRate: 100,
231 > seq: '',
232 > iKey,
233 > tags: { 'ai.operation.id': generateUuid() },
234 > data: {
235 > baseType: 'EventData',
236 > baseData: {
237 > name,
238 > // `unique_id` is a fresh per-event id (its hydro column is read by the Copilot
239 > // Telemetry Service from the snake_case `unique_id` property, NOT `uniqueId`),
240 > // mirroring the Copilot extension so each emitted event stays individually
241 > // addressable. Placed first so explicit properties still win on collision.
242 > properties: context
243 > ? { unique_id: generateUuid(), ...commonProps, ...properties, copilot_trackingId: context.trackingId } agentHostRestrictedTelemetry.ts ×5
244 > : { unique_id: generateUuid(), ...commonProps, ...properties }, agentHostRestrictedTelemetry.ts ×3
245 > measurements: measurements ?? {}, agentHostRestrictedTelemetry.ts ×8
246 > },
247 > },
248 > };
249 >
250 > this._logService.trace(`[ahp-restricted] emit ${name} (iKey ${iKey.slice(0, 8)})`);
251 >
252 > if (typeof this._fetchFn !== 'function') {
253 this._logService.warn('[ahp-restricted] global fetch unavailable; telemetry not sent');
254 return;
255 }
257 > // Fire-and-forget: post the event and move on. Delivery/robustness is intentionally kept
258 > // simple here — failures are logged, not retried (a retry loop would only mask local
259 > // telemetry-blocking resolvers, which do not exist in production).
260 > this._fetchFn(context?.endpointUrl || (context ? GH_TELEMETRY_URL : this._endpointUrl), {
261 > method: 'POST',
262 > headers: { 'Content-Type': 'application/x-json-stream' },
263 > body: JSON.stringify(envelope),
264 > }).then(res => {
265 > if (!res.ok) {
266 this._logService.warn(`[ahp-restricted] ${name} rejected: HTTP ${res.status}`);
267 }
268 > }).catch(err => { agentHostRestrictedTelemetry.ts ×8
269 this._logService.warn(`[ahp-restricted] ${name} POST failed: ${err instanceof Error ? err.message : String(err)}`);
271 > }
273 >
274 > function asString(value: string | boolean | undefined): string | undefined { agentHostRestrictedTelemetry.ts ×2
275 > return typeof value === 'string' ? value : value === undefined ? undefined : String(value);
276 > }