Skip to content
Open
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
13 changes: 13 additions & 0 deletions src/app/components/chat/chat.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@
(save)="saveUserId($event)"
></app-inline-edit>
</div>
<div class="user-menu-telemetry-row">
<span class="telemetry-label">Usage Metrics</span>
<mat-icon
class="info-icon"
matTooltip="Usage patterns, performance metrics, and environment details (OS, versions). No personal information, code, or agent data is collected.">
info_outline
</mat-icon>
<span style="flex: 1"></span>
<mat-slide-toggle
[checked]="telemetryService.telemetryEnabled()"
(change)="onTelemetryToggle($event.checked)">
</mat-slide-toggle>
</div>
</div>
</mat-menu>
</mat-toolbar>
Expand Down
21 changes: 21 additions & 0 deletions src/app/components/chat/chat.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,27 @@
gap: 4px;
}

.user-menu-telemetry-row {
display: flex;
align-items: center;
margin-top: 16px;
gap: 6px;

.telemetry-label {
font-size: 14px;
font-weight: 500;
color: var(--chat-toolbar-session-text-color);
}

.info-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: var(--chat-toolbar-icon-color);
cursor: help;
}
}

.user-menu-id {
font-family: 'Google Sans Mono', monospace;
font-size: 14px;
Expand Down
63 changes: 63 additions & 0 deletions src/app/components/chat/chat.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ import {MockMarkdownComponent} from '../markdown/testing/mock-markdown.component
import {SidePanelComponent} from '../side-panel/side-panel.component';
import {THEME_SERVICE} from '../../core/services/interfaces/theme';
import {MockThemeService} from '../../core/services/testing/mock-theme.service';
import {TelemetryService} from '../../core/services/telemetry.service';
import {TelemetryConsentDialogComponent} from '../telemetry-consent-dialog/telemetry-consent-dialog.component';
import {computed, signal} from '@angular/core';

import {ChatComponent, HIDE_SIDE_PANEL_QUERY_PARAM, INITIAL_USER_INPUT_QUERY_PARAM} from './chat.component';

Expand Down Expand Up @@ -128,9 +131,21 @@ const A2A_DATA_PART_TAG_START = '<a2a_datapart_json>';
const A2A_DATA_PART_TAG_END = '</a2a_datapart_json>';
const A2UI_MIME_TYPE = 'application/json+a2ui';

class MockTelemetryService {
readonly telemetryStatus = signal<boolean|null|undefined>(undefined);
readonly telemetryEnabled = computed(() => this.telemetryStatus() === true);
fetchTelemetryStatus = jasmine.createSpy('fetchTelemetryStatus').and.callFake(async () => {
return this.telemetryStatus();
});
setTelemetry = jasmine.createSpy('setTelemetry').and.callFake(async (enabled: boolean) => {
this.telemetryStatus.set(enabled);
});
}

describe('ChatComponent', () => {
let component: ChatComponent;
let fixture: ComponentFixture<ChatComponent>;
let mockTelemetryService: MockTelemetryService;
let mockSessionService: MockSessionService;
let mockArtifactService: MockArtifactService;
let mockWebSocketService: MockWebSocketService;
Expand All @@ -156,6 +171,7 @@ describe('ChatComponent', () => {
let mockAgentBuilderService: jasmine.SpyObj<any>;

beforeEach(async () => {
mockTelemetryService = new MockTelemetryService();
mockSessionService = new MockSessionService();
mockArtifactService = new MockArtifactService();
mockWebSocketService = new MockWebSocketService();
Expand Down Expand Up @@ -278,6 +294,7 @@ describe('ChatComponent', () => {
{provide: UI_STATE_SERVICE, useValue: mockUiStateService},
{provide: ErrorHandler, useValue: mockErrorHandler},
{provide: AGENT_BUILDER_SERVICE, useValue: mockAgentBuilderService},
{provide: TelemetryService, useValue: mockTelemetryService},
{provide: THEME_SERVICE, useClass: MockThemeService},
],
})
Expand Down Expand Up @@ -1859,5 +1876,51 @@ describe('ChatComponent', () => {
expect(component.uiEvents()[0].text).toBe('final answer');
});
});

describe('Telemetry Integration', () => {
it('should open telemetry consent dialog when telemetryStatus is null on init', fakeAsync(() => {
mockTelemetryService.telemetryStatus.set(null);

component.ngOnInit();
tick();

expect(mockDialog.open).toHaveBeenCalledWith(
TelemetryConsentDialogComponent,
jasmine.objectContaining({ disableClose: true })
);
}));

it('should not open telemetry consent dialog when telemetryStatus is true on init', fakeAsync(() => {
mockTelemetryService.telemetryStatus.set(true);

component.ngOnInit();
tick();

expect(mockDialog.open).not.toHaveBeenCalledWith(
TelemetryConsentDialogComponent,
jasmine.any(Object)
);
}));

it('should not open telemetry consent dialog when telemetryStatus is false on init', fakeAsync(() => {
mockTelemetryService.telemetryStatus.set(false);

component.ngOnInit();
tick();

expect(mockDialog.open).not.toHaveBeenCalledWith(
TelemetryConsentDialogComponent,
jasmine.any(Object)
);
}));

it('should call setTelemetry when onTelemetryToggle is called', async () => {
await component.onTelemetryToggle(true);
expect(mockTelemetryService.setTelemetry).toHaveBeenCalledWith(true);

await component.onTelemetryToggle(false);
expect(mockTelemetryService.setTelemetry).toHaveBeenCalledWith(false);
});
});
});
});
21 changes: 21 additions & 0 deletions src/app/components/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ import { SidePanelComponent } from '../side-panel/side-panel.component';
import { ViewImageDialogComponent } from '../view-image-dialog/view-image-dialog.component';
import { InlineEditComponent } from '../inline-edit/inline-edit.component';
import { FormatMetricNamePipe } from '../eval-tab/format-metric-name.pipe';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { TelemetryService } from '../../core/services/telemetry.service';
import { TelemetryConsentDialogComponent } from '../telemetry-consent-dialog/telemetry-consent-dialog.component';

import { ChatMessagesInjectionToken } from './chat.component.i18n';
import { SidePanelMessagesInjectionToken } from '../side-panel/side-panel.component.i18n';
Expand Down Expand Up @@ -181,6 +184,8 @@ const BIDI_STREAMING_IN_PROGRESS_WARNING =
SessionTabComponent,
InlineEditComponent,
FormatMetricNamePipe,
MatSlideToggleModule,
TelemetryConsentDialogComponent,
],
})
export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
Expand Down Expand Up @@ -214,6 +219,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
protected readonly uiStateService = inject(UI_STATE_SERVICE);
protected readonly agentBuilderService = inject(AGENT_BUILDER_SERVICE);
protected readonly themeService = inject(THEME_SERVICE, { optional: true });
protected readonly telemetryService = inject(TelemetryService);
protected readonly logoComponent: Type<Component> | null = inject(LOGO_COMPONENT, {
optional: true,
});
Expand Down Expand Up @@ -860,6 +866,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
});
this.checkTelemetryConsent();
}

get sessionTab() {
Expand All @@ -870,6 +877,20 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
this.onViewModeChange('traces');
}

private async checkTelemetryConsent(): Promise<void> {
const consent = await this.telemetryService.fetchTelemetryStatus();
if (consent === null) {
this.dialog.open(TelemetryConsentDialogComponent, {
disableClose: true,
panelClass: 'telemetry-consent-dialog-panel',
});
}
}

async onTelemetryToggle(checked: boolean): Promise<void> {
await this.telemetryService.setTelemetry(checked);
}

ngAfterViewInit() {
if (this.showSidePanel) {
this.sideDrawer()?.open();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<h2 mat-dialog-title class="dialog-title">Help Improve ADK!</h2>
<mat-dialog-content class="dialog-content">
<p>To help us make ADK better, please consider allowing Google to collect pseudonymized usage data from your interactions with both ADK Web UI and CLI. This data helps us understand how features are used, identify areas for improvement, and prioritize development.</p>

<div class="info-section">
<strong>What we collect:</strong> Usage patterns, performance metrics, and environment details (OS, versions). We do not collect any personal information, code, or agent data.
</div>

<div class="info-section">
<strong>Your Choice:</strong> This is OFF by default. Your participation is optional.
</div>

<div class="info-section">
<strong>Control:</strong> You can change this setting at any time via the toggle in the Web UI User Settings or by using the CLI command (<code>adk telemetry disable</code>).
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" class="dialog-actions">
<button mat-button (click)="onNoThanks()">No Thanks</button>
<button mat-flat-button color="primary" (click)="onEnable()">Enable</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.dialog-title {
font-family: 'Google Sans', 'Google Sans Logo', sans-serif;
font-size: 20px;
font-weight: 500;
margin-bottom: 8px;
color: var(--mdc-dialog-title-text-color, inherit);
}

.dialog-content {
font-family: Roboto, sans-serif;
font-size: 14px;
color: var(--mdc-dialog-supporting-text-color, inherit);
line-height: 1.5;

p {
margin-bottom: 12px;
color: var(--mdc-dialog-supporting-text-color, inherit);
}

.info-section {
background-color: var(--builder-form-field-background-color, rgba(138, 180, 248, 0.08));
border: 1px solid var(--builder-border-color, rgba(138, 180, 248, 0.2));
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 10px;
color: var(--mdc-dialog-supporting-text-color, inherit);

strong {
color: var(--builder-text-link-color, #8ab4f8);
font-weight: 500;
}
}
}

.dialog-actions {
padding: 16px 24px;
gap: 8px;

button {
font-family: 'Google Sans', sans-serif;
font-weight: 500;
}

button[color="primary"] {
background-color: var(--builder-text-link-color, #1a73e8) !important;
color: var(--mat-sys-background, #ffffff) !important;

&:hover {
opacity: 0.9;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* @license
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatDialogRef} from '@angular/material/dialog';
import {provideNoopAnimations} from '@angular/platform-browser/animations';
// 1p-ONLY-IMPORTS: import {beforeEach, describe, expect, it}

import {TelemetryService} from '../../core/services/telemetry.service';
import {initTestBed} from '../../testing/utils';

import {TelemetryConsentDialogComponent} from './telemetry-consent-dialog.component';

describe('TelemetryConsentDialogComponent', () => {
let component: TelemetryConsentDialogComponent;
let fixture: ComponentFixture<TelemetryConsentDialogComponent>;
let mockTelemetryService: jasmine.SpyObj<TelemetryService>;
let mockDialogRef:
jasmine.SpyObj<MatDialogRef<TelemetryConsentDialogComponent>>;

initTestBed();

beforeEach(async () => {
mockTelemetryService = jasmine.createSpyObj<TelemetryService>(
'TelemetryService', ['setTelemetry']);
mockDialogRef =
jasmine.createSpyObj<MatDialogRef<TelemetryConsentDialogComponent>>(
'MatDialogRef', ['close']);

await TestBed
.configureTestingModule({
imports: [TelemetryConsentDialogComponent],
providers: [
{provide: TelemetryService, useValue: mockTelemetryService},
{provide: MatDialogRef, useValue: mockDialogRef},
provideNoopAnimations(),
],
})
.compileComponents();

fixture = TestBed.createComponent(TelemetryConsentDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it(
'should set telemetry to true and close when enable clicked',
() => {
component.onEnable();
expect(mockTelemetryService.setTelemetry).toHaveBeenCalledWith(true);
expect(mockDialogRef.close).toHaveBeenCalledWith(true);
});

it(
'should set telemetry to false and close when no thanks clicked',
() => {
component.onNoThanks();
expect(mockTelemetryService.setTelemetry).toHaveBeenCalledWith(false);
expect(mockDialogRef.close).toHaveBeenCalledWith(false);
});

it(
'should close dialog without setting preference when dismissed', () => {
component.onDismiss();
expect(mockTelemetryService.setTelemetry).not.toHaveBeenCalled();
expect(mockDialogRef.close).toHaveBeenCalledWith();
});
});
Loading