Skip to content

Commit b725ded

Browse files
benibenjsbatten
andauthored
Add support for extension version filter assignments (microsoft#264012)
* Add support for extension version filter assignments * fix gdpr? * adding account based filters to exp --------- Co-authored-by: SteVen Batten <sbatten@microsoft.com>
1 parent 335e03d commit b725ded

File tree

4 files changed

+279
-15
lines changed

4 files changed

+279
-15
lines changed

src/vs/workbench/contrib/chat/common/chatContextKeys.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,16 @@ export namespace ChatContextKeys {
7373

7474
export const Entitlement = {
7575
internal: new RawContextKey<boolean>('chatEntitlementInternal', false, true), // True when user is a chat internal user.
76+
gitHubInternal: new RawContextKey<boolean>('chatEntitlementGitHubInternal', false, true), // True when user is a GitHub internal user.
77+
microsoftInternal: new RawContextKey<boolean>('chatEntitlementMicrosoftInternal', false, true), // True when user is a Microsoft internal user.
7678
signedOut: new RawContextKey<boolean>('chatEntitlementSignedOut', false, true), // True when user is signed out.
7779
canSignUp: new RawContextKey<boolean>('chatPlanCanSignUp', false, true), // True when user can sign up to be a chat free user.
7880
free: new RawContextKey<boolean>('chatPlanFree', false, true), // True when user is a chat free user.
7981
pro: new RawContextKey<boolean>('chatPlanPro', false, true), // True when user is a chat pro user.
8082
proPlus: new RawContextKey<boolean>('chatPlanProPlus', false, true), // True when user is a chat pro plus user.
8183
business: new RawContextKey<boolean>('chatPlanBusiness', false, true), // True when user is a chat business user.
82-
enterprise: new RawContextKey<boolean>('chatPlanEnterprise', false, true) // True when user is a chat enterprise user.
84+
enterprise: new RawContextKey<boolean>('chatPlanEnterprise', false, true), // True when user is a chat enterprise user.
85+
sku: new RawContextKey<string>('chatEntitlementSku', undefined, true), // The raw SKU string from the entitlement service.
8386
};
8487

8588
export const chatQuotaExceeded = new RawContextKey<boolean>('chatQuotaExceeded', false, true);

src/vs/workbench/contrib/chat/common/chatEntitlementService.ts

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ export interface IChatEntitlementService {
103103

104104
readonly entitlement: ChatEntitlement;
105105
readonly isInternal: boolean;
106+
readonly sku: string | undefined;
107+
readonly isGitHubInternal: boolean;
108+
readonly isMicrosoftInternal: boolean;
106109

107110
readonly onDidChangeQuotaExceeded: Event<void>;
108111
readonly onDidChangeQuotaRemaining: Event<void>;
@@ -179,7 +182,11 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme
179182
ChatContextKeys.Entitlement.proPlus.key,
180183
ChatContextKeys.Entitlement.free.key,
181184
ChatContextKeys.Entitlement.canSignUp.key,
182-
ChatContextKeys.Entitlement.signedOut.key
185+
ChatContextKeys.Entitlement.signedOut.key,
186+
ChatContextKeys.Entitlement.internal.key,
187+
ChatContextKeys.Entitlement.gitHubInternal.key,
188+
ChatContextKeys.Entitlement.microsoftInternal.key,
189+
ChatContextKeys.Entitlement.sku.key
183190
])), this._store
184191
), () => { }, this._store
185192
);
@@ -246,6 +253,18 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme
246253
return this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.internal.key) === true;
247254
}
248255

256+
get isGitHubInternal(): boolean {
257+
return this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.gitHubInternal.key) === true;
258+
}
259+
260+
get isMicrosoftInternal(): boolean {
261+
return this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.Entitlement.microsoftInternal.key) === true;
262+
}
263+
264+
get sku(): string | undefined {
265+
return this.contextKeyService.getContextKeyValue<string>(ChatContextKeys.Entitlement.sku.key);
266+
}
267+
249268
//#endregion
250269

251270
//#region --- Quotas
@@ -406,6 +425,9 @@ interface IEntitlementsResponse extends ILegacyQuotaSnapshotResponse {
406425
interface IEntitlements {
407426
readonly entitlement: ChatEntitlement;
408427
readonly isInternal?: boolean;
428+
readonly isGitHubInternal?: boolean;
429+
readonly isMicrosoftInternal?: boolean;
430+
readonly sku?: string;
409431
readonly quotas?: IQuotas;
410432
}
411433

@@ -651,8 +673,11 @@ export class ChatEntitlementRequests extends Disposable {
651673

652674
const entitlements: IEntitlements = {
653675
entitlement,
654-
isInternal: this.containsInternalOrgs(entitlementsResponse.organization_login_list),
655-
quotas: this.toQuotas(entitlementsResponse)
676+
isInternal: this.containsInternalOrgs(entitlementsResponse.organization_login_list, ['github', 'microsoft']),
677+
isGitHubInternal: this.containsInternalOrgs(entitlementsResponse.organization_login_list, ['github']),
678+
isMicrosoftInternal: this.containsInternalOrgs(entitlementsResponse.organization_login_list, ['microsoft']),
679+
quotas: this.toQuotas(entitlementsResponse),
680+
sku: entitlementsResponse.access_type_sku
656681
};
657682

658683
this.logService.trace(`[chat entitlement]: resolved to ${entitlements.entitlement}, quotas: ${JSON.stringify(entitlements.quotas)}`);
@@ -740,9 +765,7 @@ export class ChatEntitlementRequests extends Disposable {
740765
return quotas;
741766
}
742767

743-
private containsInternalOrgs(organizationLogins: string[]): boolean {
744-
const internalOrgs = ['github', 'microsoft'];
745-
768+
private containsInternalOrgs(organizationLogins: string[], internalOrgs: string[]): boolean {
746769
return organizationLogins.some(org => internalOrgs.includes(org));
747770
}
748771

@@ -787,7 +810,7 @@ export class ChatEntitlementRequests extends Disposable {
787810
private update(state: IEntitlements): void {
788811
this.state = state;
789812

790-
this.context.update({ entitlement: this.state.entitlement, isInternal: !!this.state.isInternal });
813+
this.context.update({ entitlement: this.state.entitlement, isInternal: !!this.state.isInternal, isGitHubInternal: !!this.state.isGitHubInternal, isMicrosoftInternal: !!this.state.isMicrosoftInternal, sku: this.state.sku });
791814

792815
if (state.quotas) {
793816
this.chatQuotasAccessor.acceptQuotas(state.quotas);
@@ -942,11 +965,26 @@ export interface IChatEntitlementContextState extends IChatSentiment {
942965
*/
943966
entitlement: ChatEntitlement;
944967

968+
/**
969+
* User's last known or resolved raw SKU type.
970+
*/
971+
sku: string | undefined;
972+
945973
/**
946974
* User is an internal chat user.
947975
*/
948976
isInternal: boolean;
949977

978+
/**
979+
* User is a GitHub internal user.
980+
*/
981+
isGitHubInternal: boolean;
982+
983+
/**
984+
* User is a Microsoft internal user.
985+
*/
986+
isMicrosoftInternal: boolean;
987+
950988
/**
951989
* User is or was a registered Chat user.
952990
*/
@@ -968,6 +1006,9 @@ export class ChatEntitlementContext extends Disposable {
9681006
private readonly enterpriseContextKey: IContextKey<boolean>;
9691007

9701008
private readonly isInternalContextKey: IContextKey<boolean>;
1009+
private readonly isGitHubInternalContextKey: IContextKey<boolean>;
1010+
private readonly isMicrosoftInternalContextKey: IContextKey<boolean>;
1011+
private readonly skuContextKey: IContextKey<string | undefined>;
9711012

9721013
private readonly hiddenContext: IContextKey<boolean>;
9731014
private readonly laterContext: IContextKey<boolean>;
@@ -1002,13 +1043,16 @@ export class ChatEntitlementContext extends Disposable {
10021043
this.businessContextKey = ChatContextKeys.Entitlement.business.bindTo(contextKeyService);
10031044
this.enterpriseContextKey = ChatContextKeys.Entitlement.enterprise.bindTo(contextKeyService);
10041045
this.isInternalContextKey = ChatContextKeys.Entitlement.internal.bindTo(contextKeyService);
1046+
this.isGitHubInternalContextKey = ChatContextKeys.Entitlement.gitHubInternal.bindTo(contextKeyService);
1047+
this.isMicrosoftInternalContextKey = ChatContextKeys.Entitlement.microsoftInternal.bindTo(contextKeyService);
1048+
this.skuContextKey = ChatContextKeys.Entitlement.sku.bindTo(contextKeyService);
10051049
this.hiddenContext = ChatContextKeys.Setup.hidden.bindTo(contextKeyService);
10061050
this.laterContext = ChatContextKeys.Setup.later.bindTo(contextKeyService);
10071051
this.installedContext = ChatContextKeys.Setup.installed.bindTo(contextKeyService);
10081052
this.disabledContext = ChatContextKeys.Setup.disabled.bindTo(contextKeyService);
10091053
this.untrustedContext = ChatContextKeys.Setup.untrusted.bindTo(contextKeyService);
10101054

1011-
this._state = this.storageService.getObject<IChatEntitlementContextState>(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown, isInternal: false };
1055+
this._state = this.storageService.getObject<IChatEntitlementContextState>(ChatEntitlementContext.CHAT_ENTITLEMENT_CONTEXT_STORAGE_KEY, StorageScope.PROFILE) ?? { entitlement: ChatEntitlement.Unknown, isInternal: false, isGitHubInternal: false, isMicrosoftInternal: false, sku: undefined };
10121056

10131057
this.checkExtensionInstallation();
10141058
this.updateContextSync();
@@ -1069,8 +1113,8 @@ export class ChatEntitlementContext extends Disposable {
10691113
update(context: { installed: boolean; disabled: boolean; untrusted: boolean }): Promise<void>;
10701114
update(context: { hidden: false }): Promise<void>; // legacy UI state from before we had a setting to hide, keep around to still support users who used this
10711115
update(context: { later: boolean }): Promise<void>;
1072-
update(context: { entitlement: ChatEntitlement; isInternal: boolean }): Promise<void>;
1073-
update(context: { installed?: boolean; disabled?: boolean; untrusted?: boolean; hidden?: false; later?: boolean; entitlement?: ChatEntitlement; isInternal?: boolean }): Promise<void> {
1116+
update(context: { entitlement: ChatEntitlement; isInternal: boolean; isGitHubInternal: boolean; isMicrosoftInternal: boolean; sku: string | undefined }): Promise<void>;
1117+
update(context: { installed?: boolean; disabled?: boolean; untrusted?: boolean; hidden?: false; later?: boolean; entitlement?: ChatEntitlement; isInternal?: boolean; isGitHubInternal?: boolean; isMicrosoftInternal?: boolean; sku?: string }): Promise<void> {
10741118
this.logService.trace(`[chat entitlement context] update(): ${JSON.stringify(context)}`);
10751119

10761120
if (typeof context.installed === 'boolean' && typeof context.disabled === 'boolean' && typeof context.untrusted === 'boolean') {
@@ -1094,6 +1138,9 @@ export class ChatEntitlementContext extends Disposable {
10941138
if (typeof context.entitlement === 'number') {
10951139
this._state.entitlement = context.entitlement;
10961140
this._state.isInternal = !!context.isInternal;
1141+
this._state.isGitHubInternal = !!context.isGitHubInternal;
1142+
this._state.isMicrosoftInternal = !!context.isMicrosoftInternal;
1143+
this._state.sku = context.sku;
10971144

10981145
if (this._state.entitlement === ChatEntitlement.Free || isProUser(this._state.entitlement)) {
10991146
this._state.registered = true;
@@ -1129,6 +1176,9 @@ export class ChatEntitlementContext extends Disposable {
11291176
this.businessContextKey.set(state.entitlement === ChatEntitlement.Business);
11301177
this.enterpriseContextKey.set(state.entitlement === ChatEntitlement.Enterprise);
11311178
this.isInternalContextKey.set(state.isInternal);
1179+
this.isGitHubInternalContextKey.set(state.isGitHubInternal);
1180+
this.isMicrosoftInternalContextKey.set(state.isMicrosoftInternal);
1181+
this.skuContextKey.set(state.sku);
11321182
this.hiddenContext.set(!!state.hidden);
11331183
this.laterContext.set(!!state.later);
11341184
this.installedContext.set(!!state.installed);
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*---------------------------------------------------------------------------------------------
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 type { IExperimentationFilterProvider } from 'tas-client-umd';
7+
import { IExtensionService } from '../../extensions/common/extensions.js';
8+
import { Disposable } from '../../../../base/common/lifecycle.js';
9+
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
10+
import { ILogService } from '../../../../platform/log/common/log.js';
11+
import { Emitter } from '../../../../base/common/event.js';
12+
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
13+
// eslint-disable-next-line local/code-import-patterns
14+
import { IChatEntitlementService } from '../../../contrib/chat/common/chatEntitlementService.js';
15+
16+
export enum ExtensionsFilter {
17+
18+
/**
19+
* Version of the github.copilot extension.
20+
*/
21+
CopilotExtensionVersion = 'X-Copilot-RelatedPluginVersion-githubcopilot',
22+
23+
/**
24+
* Version of the github.copilot-chat extension.
25+
*/
26+
CopilotChatExtensionVersion = 'X-Copilot-RelatedPluginVersion-githubcopilotchat',
27+
28+
/**
29+
* Version of the completions version.
30+
*/
31+
CompletionsVersionInCopilotChat = 'X-VSCode-CompletionsInChatExtensionVersion',
32+
33+
/**
34+
* SKU of the copilot entitlement.
35+
*/
36+
CopilotSku = 'X-GitHub-Copilot-SKU',
37+
38+
/**
39+
* The internal org of the user.
40+
*/
41+
MicrosoftInternalOrg = 'X-Microsoft-Internal-Org',
42+
}
43+
44+
enum StorageVersionKeys {
45+
CopilotExtensionVersion = 'extensionsAssignmentFilterProvider.copilotExtensionVersion',
46+
CopilotChatExtensionVersion = 'extensionsAssignmentFilterProvider.copilotChatExtensionVersion',
47+
CompletionsVersion = 'extensionsAssignmentFilterProvider.copilotCompletionsVersion',
48+
CopilotSku = 'extensionsAssignmentFilterProvider.copilotSku',
49+
CopilotInternalOrg = 'extensionsAssignmentFilterProvider.copilotInternalOrg',
50+
}
51+
52+
export class CopilotAssignmentFilterProvider extends Disposable implements IExperimentationFilterProvider {
53+
private copilotChatExtensionVersion: string | undefined;
54+
private copilotExtensionVersion: string | undefined;
55+
// TODO@benibenj remove this when completions have been ported to chat
56+
private copilotCompletionsVersion: string | undefined;
57+
58+
private copilotInternalOrg: string | undefined;
59+
private copilotSku: string | undefined;
60+
61+
private readonly _onDidChangeFilters = this._register(new Emitter<void>());
62+
readonly onDidChangeFilters = this._onDidChangeFilters.event;
63+
64+
constructor(
65+
@IExtensionService private readonly _extensionService: IExtensionService,
66+
@ILogService private readonly _logService: ILogService,
67+
@IStorageService private readonly _storageService: IStorageService,
68+
@IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService,
69+
) {
70+
super();
71+
72+
this.copilotExtensionVersion = this._storageService.get(StorageVersionKeys.CopilotExtensionVersion, StorageScope.PROFILE);
73+
this.copilotChatExtensionVersion = this._storageService.get(StorageVersionKeys.CopilotChatExtensionVersion, StorageScope.PROFILE);
74+
this.copilotCompletionsVersion = this._storageService.get(StorageVersionKeys.CompletionsVersion, StorageScope.PROFILE);
75+
this.copilotSku = this._storageService.get(StorageVersionKeys.CopilotSku, StorageScope.PROFILE);
76+
this.copilotInternalOrg = this._storageService.get(StorageVersionKeys.CopilotInternalOrg, StorageScope.PROFILE);
77+
78+
this._register(this._extensionService.onDidChangeExtensionsStatus(extensionIdentifiers => {
79+
if (extensionIdentifiers.some(identifier => ExtensionIdentifier.equals(identifier, 'github.copilot') || ExtensionIdentifier.equals(identifier, 'github.copilot-chat'))) {
80+
this.updateExtensionVersions();
81+
}
82+
}));
83+
84+
this._register(this._chatEntitlementService.onDidChangeEntitlement(() => {
85+
this.updateCopilotEntitlementInfo();
86+
}));
87+
88+
this.updateExtensionVersions();
89+
this.updateCopilotEntitlementInfo();
90+
}
91+
92+
private async updateExtensionVersions() {
93+
let copilotExtensionVersion;
94+
let copilotChatExtensionVersion;
95+
let copilotCompletionsVersion;
96+
97+
try {
98+
const [copilotExtension, copilotChatExtension] = await Promise.all([
99+
this._extensionService.getExtension('github.copilot'),
100+
this._extensionService.getExtension('github.copilot-chat'),
101+
]);
102+
103+
copilotExtensionVersion = copilotExtension?.version;
104+
copilotChatExtensionVersion = copilotChatExtension?.version;
105+
copilotCompletionsVersion = (copilotChatExtension as any)?.completionsCoreVersion;
106+
} catch (error) {
107+
this._logService.error('Failed to update extension version assignments', error);
108+
}
109+
110+
if (this.copilotCompletionsVersion === copilotCompletionsVersion &&
111+
this.copilotExtensionVersion === copilotExtensionVersion &&
112+
this.copilotChatExtensionVersion === copilotChatExtensionVersion) {
113+
return;
114+
}
115+
116+
this.copilotExtensionVersion = copilotExtensionVersion;
117+
this.copilotChatExtensionVersion = copilotChatExtensionVersion;
118+
this.copilotCompletionsVersion = copilotCompletionsVersion;
119+
120+
this._storageService.store(StorageVersionKeys.CopilotExtensionVersion, this.copilotExtensionVersion, StorageScope.PROFILE, StorageTarget.MACHINE);
121+
this._storageService.store(StorageVersionKeys.CopilotChatExtensionVersion, this.copilotChatExtensionVersion, StorageScope.PROFILE, StorageTarget.MACHINE);
122+
this._storageService.store(StorageVersionKeys.CompletionsVersion, this.copilotCompletionsVersion, StorageScope.PROFILE, StorageTarget.MACHINE);
123+
124+
// Notify that the filters have changed.
125+
this._onDidChangeFilters.fire();
126+
}
127+
128+
private updateCopilotEntitlementInfo() {
129+
const newSku = this._chatEntitlementService.sku;
130+
const newInternalOrg = this._chatEntitlementService.isGitHubInternal ? 'github' : (this._chatEntitlementService.isMicrosoftInternal ? 'microsoft' : undefined);
131+
132+
if (this.copilotSku === newSku && this.copilotInternalOrg === newInternalOrg) {
133+
return;
134+
}
135+
136+
this.copilotSku = newSku;
137+
this.copilotInternalOrg = newInternalOrg;
138+
139+
this._storageService.store(StorageVersionKeys.CopilotSku, this.copilotSku, StorageScope.PROFILE, StorageTarget.MACHINE);
140+
this._storageService.store(StorageVersionKeys.CopilotInternalOrg, this.copilotInternalOrg, StorageScope.PROFILE, StorageTarget.MACHINE);
141+
142+
// Notify that the filters have changed.
143+
this._onDidChangeFilters.fire();
144+
}
145+
146+
/**
147+
* Returns a version string that can be parsed by the TAS client.
148+
* The tas client cannot handle suffixes lke "-insider"
149+
* Ref: https://github.com/microsoft/tas-client/blob/30340d5e1da37c2789049fcf45928b954680606f/vscode-tas-client/src/vscode-tas-client/VSCodeFilterProvider.ts#L35
150+
*
151+
* @param version Version string to be trimmed.
152+
*/
153+
private static trimVersionSuffix(version: string): string {
154+
const regex = /\-[a-zA-Z0-9]+$/;
155+
const result = version.split(regex);
156+
157+
return result[0];
158+
}
159+
160+
getFilterValue(filter: string): string | null {
161+
switch (filter) {
162+
case ExtensionsFilter.CopilotExtensionVersion:
163+
return this.copilotExtensionVersion ? CopilotAssignmentFilterProvider.trimVersionSuffix(this.copilotExtensionVersion) : null;
164+
case ExtensionsFilter.CompletionsVersionInCopilotChat:
165+
return this.copilotCompletionsVersion ? CopilotAssignmentFilterProvider.trimVersionSuffix(this.copilotCompletionsVersion) : null;
166+
case ExtensionsFilter.CopilotChatExtensionVersion:
167+
return this.copilotChatExtensionVersion ? CopilotAssignmentFilterProvider.trimVersionSuffix(this.copilotChatExtensionVersion) : null;
168+
case ExtensionsFilter.CopilotSku:
169+
return this.copilotSku ?? null;
170+
case ExtensionsFilter.MicrosoftInternalOrg:
171+
return this.copilotInternalOrg ?? null;
172+
default:
173+
return null;
174+
}
175+
}
176+
177+
getFilters(): Map<string, any> {
178+
const filters: Map<string, any> = new Map<string, any>();
179+
const filterValues = Object.values(ExtensionsFilter);
180+
for (const value of filterValues) {
181+
filters.set(value, this.getFilterValue(value));
182+
}
183+
184+
return filters;
185+
}
186+
}

0 commit comments

Comments
 (0)