Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export class ChatQuotaNotificationContribution extends Disposable implements IWo

this._setNotification({
id: QUOTA_NOTIFICATION_ID,
telemetryId: 'quotaExhausted',
severity: ChatInputNotificationSeverity.Info,
message: localize('quota.exhausted.title', "Credit Limit Reached"),
description,
Expand All @@ -249,6 +250,7 @@ export class ChatQuotaNotificationContribution extends Disposable implements IWo

this._setNotification({
id: QUOTA_NOTIFICATION_ID,
telemetryId: 'overageActivation',
severity: ChatInputNotificationSeverity.Info,
message: localize('quota.overage.title', "Credit Limit Reached"),
description: localize('quota.overage.desc', "Additional budget is now covering extra usage."),
Expand Down Expand Up @@ -285,6 +287,7 @@ export class ChatQuotaNotificationContribution extends Disposable implements IWo

this._setNotification({
id: QUOTA_NOTIFICATION_ID,
telemetryId: 'quotaApproaching',
severity: ChatInputNotificationSeverity.Info,
message: localize('quota.approaching.title', "Credits at {0}%", warning.percentUsed),
description,
Expand Down Expand Up @@ -344,6 +347,7 @@ export class ChatQuotaNotificationContribution extends Disposable implements IWo

this._setNotification({
id: QUOTA_NOTIFICATION_ID,
telemetryId: warning.type === 'session' ? 'sessionRateLimitWarning' : 'weeklyRateLimitWarning',
severity: ChatInputNotificationSeverity.Info,
message,
description,
Expand Down Expand Up @@ -378,6 +382,7 @@ export class ChatQuotaNotificationContribution extends Disposable implements IWo

this._setNotification({
id: QUOTA_NOTIFICATION_ID,
telemetryId: 'managedPlanBlocked',
severity: ChatInputNotificationSeverity.Info,
message: localize('quota.blocked.managed.title', "Usage Blocked"),
description: localize('quota.blocked.managed', "Your organization or enterprise has exceeded its Copilot budget. Contact your admin to resume usage."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface IChatInputNotificationMuteAction {

export interface IChatInputNotification {
readonly id: string;
readonly telemetryId?: string;
readonly severity: ChatInputNotificationSeverity;
readonly message: string | IMarkdownString;
readonly description: string | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ import './media/chatInputNotificationWidget.css';

const $ = dom.$;

type ChatInputNotificationTelemetryEvent = {
id: string;
telemetryId?: string;
};

type ChatInputNotificationTelemetryClassification = {
id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the chat input notification.' };
telemetryId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The feature-provided identifier for the notification message that was shown or dismissed.' };
owner: 'rfeltis';
comment: 'Tracks chat input notification visibility and user dismissals.';
};

const severityToClass: Record<ChatInputNotificationSeverity, string> = {
[ChatInputNotificationSeverity.Info]: 'severity-info',
[ChatInputNotificationSeverity.Warning]: 'severity-warning',
Expand All @@ -44,6 +56,7 @@ export class ChatInputNotificationWidget extends Disposable {
readonly domNode: HTMLElement;

private readonly _contentDisposables = this._register(new DisposableStore());
private _lastShownTelemetryData: ChatInputNotificationTelemetryEvent | undefined;

/**
* Optional provider that returns the current session type of the owning
Expand Down Expand Up @@ -82,11 +95,13 @@ export class ChatInputNotificationWidget extends Disposable {
const notification = this._notificationService.getActiveNotification(n => this._matchesSession(n));
if (!notification) {
this.domNode.parentElement?.classList.remove('has-notification');
this._lastShownTelemetryData = undefined;
return;
}

this.domNode.parentElement?.classList.add('has-notification');
this._renderNotification(notification);
this._logShownTelemetry(notification);
}

private _matchesSession(notification: IChatInputNotification): boolean {
Expand Down Expand Up @@ -162,7 +177,10 @@ export class ChatInputNotificationWidget extends Disposable {
// browser has finished propagating the click event. Otherwise
// blur handlers fired by removing the button from focus can
// move/remove nodes that `clearNode` then trips over.
const dismiss = () => queueMicrotask(() => this._notificationService.dismissNotification(notification.id));
const dismiss = () => queueMicrotask(() => {
this._telemetryService.publicLog2<ChatInputNotificationTelemetryEvent, ChatInputNotificationTelemetryClassification>('chatInputNotificationDismissed', this._getTelemetryData(notification));
this._notificationService.dismissNotification(notification.id);
});
this._contentDisposables.add(dom.addDisposableListener(dismissButton, dom.EventType.CLICK, dismiss));
this._contentDisposables.add(dom.addDisposableListener(dismissButton, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
Expand Down Expand Up @@ -218,4 +236,20 @@ export class ChatInputNotificationWidget extends Disposable {
}
}
}

private _logShownTelemetry(notification: IChatInputNotification): void {
const data = this._getTelemetryData(notification);
if (this._lastShownTelemetryData?.id === data.id && this._lastShownTelemetryData.telemetryId === data.telemetryId) {
return;
}
this._lastShownTelemetryData = data;
this._telemetryService.publicLog2<ChatInputNotificationTelemetryEvent, ChatInputNotificationTelemetryClassification>('chatInputNotificationShown', data);
}

private _getTelemetryData(notification: IChatInputNotification): ChatInputNotificationTelemetryEvent {
return {
id: notification.id,
telemetryId: notification.telemetryId,
};
}
}
Loading
Loading