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
3 changes: 3 additions & 0 deletions src/app/components/builder-tabs/builder-tabs.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { MatIcon } from '@angular/material/icon';
import { MatInput } from '@angular/material/input';
import { MatOption, MatSelect } from '@angular/material/select';
import { SnackbarService } from '../../core/services/snackbar.service';
import { AnalyticsService } from '../../core/services/analytics.service';
import {MatExpansionModule} from '@angular/material/expansion';
import { MatTooltip } from '@angular/material/tooltip';
import { Router } from '@angular/router';
Expand Down Expand Up @@ -129,6 +130,7 @@ export class BuilderTabsComponent {
private snackBar = inject(SnackbarService);
private router = inject(Router);
private cdr = inject(ChangeDetectorRef);
private analyticsService = inject(AnalyticsService);

protected selectedTool: ToolNode | undefined = undefined;
protected toolAgentName: string = '';
Expand Down Expand Up @@ -802,6 +804,7 @@ export class BuilderTabsComponent {
if (success) {
this.agentService.agentBuild(appName, formData).subscribe((success) => {
if (success) {
this.analyticsService.sendEvent('builder_agent_save_click');
this.router.navigate(['/'], {
queryParams: { app: appName }
}).then(() => {
Expand Down
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);
});
});
});
});
30 changes: 30 additions & 0 deletions src/app/components/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { ListResponse } from '../../core/services/interfaces/types';
import { UI_STATE_SERVICE } from '../../core/services/interfaces/ui-state';
import { LOCATION_SERVICE } from '../../core/services/location.service';
import { TestsService } from '../../core/services/tests.service';
import { AnalyticsService } from '../../core/services/analytics.service';
import { ResizableDrawerDirective } from '../../directives/resizable-drawer.directive';
import { AddItemDialogComponent } from '../add-item-dialog/add-item-dialog.component';
import { AgentStructureGraphDialogComponent } from '../agent-structure-graph-dialog/agent-structure-graph-dialog';
Expand All @@ -84,6 +85,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 +185,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 +220,8 @@ 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 analyticsService = inject(AnalyticsService);
protected readonly logoComponent: Type<Component> | null = inject(LOGO_COMPONENT, {
optional: true,
});
Expand Down Expand Up @@ -761,6 +769,10 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
this.agentService.getVersion().subscribe((res) => {
this.adkVersion.set(res.version || '');
this.versionInfo.set(res);
this.analyticsService.setUserProperties({
adk_version: res?.version || '',
adk_language: res?.language || '',
});
});

combineLatest([
Expand Down Expand Up @@ -860,6 +872,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
});
this.checkTelemetryConsent();
}

get sessionTab() {
Expand All @@ -870,6 +883,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 Expand Up @@ -1084,6 +1111,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
this.sessionId = res.id ?? '';
this.sessionTab?.refreshSession();
this.sessionTab?.reloadSession(this.sessionId);
this.analyticsService.sendEvent('chat_session_create');

this.isSessionUrlEnabledObs.subscribe((enabled) => {
if (enabled) {
Expand Down Expand Up @@ -3085,6 +3113,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
.toString();
this.location.replaceState(url);
this.isBuilderMode.set(true);
this.analyticsService.sendEvent('builder_enter_click');

// Load existing agent configuration if app is selected
if (this.appName) {
Expand Down Expand Up @@ -3146,6 +3175,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
openAgentStructureGraphDialog(mode: 'session' | 'event' = 'session'): void {
this.agentStructureOverlayMode = mode;
this.showAgentStructureOverlay = true;
this.analyticsService.sendEvent('graph_view_click');
}

saveAgentBuilder() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose } from '@angular/material/dialog';
import {uuidv4} from 'uuidv7';
import {EVAL_SERVICE} from '../../../../core/services/interfaces/eval';
import {AnalyticsService} from '../../../../core/services/analytics.service';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { MatFormField } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
Expand Down Expand Up @@ -55,6 +56,7 @@ export class AddEvalSessionDialogComponent {
existingCases?: string[];
} = inject(MAT_DIALOG_DATA);
readonly dialogRef = inject(MatDialogRef<AddEvalSessionDialogComponent>);
private readonly analyticsService = inject(AnalyticsService);

newCaseId: string = this.data.defaultName || ('case_' + uuidv4().slice(0, 6));
loading = false;
Expand All @@ -81,6 +83,7 @@ export class AddEvalSessionDialogComponent {
)
.subscribe({
next: (res) => {
this.analyticsService.sendEvent('eval_create_click');
this.dialogRef.close(true);
},
error: (err) => {
Expand Down
8 changes: 8 additions & 0 deletions src/app/components/side-panel/side-panel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {TraceTabComponent} from '../trace-tab/trace-tab.component';
import {EventTabComponent} from '../event-tab/event-tab.component';

import {TRACE_SERVICE} from '../../core/services/interfaces/trace';
import {AnalyticsService} from '../../core/services/analytics.service';
import {SidePanelMessagesInjectionToken} from './side-panel.component.i18n';

/**
Expand Down Expand Up @@ -119,6 +120,7 @@ export class SidePanelComponent implements AfterViewInit, OnInit {
viewChild('evalTabContainer', {read: ViewContainerRef});
readonly tabGroup = viewChild(MatTabGroup);

protected readonly analyticsService = inject(AnalyticsService);
readonly logoComponent: Type<Component>|null = inject(LOGO_COMPONENT, {
optional: true,
});
Expand Down Expand Up @@ -199,6 +201,12 @@ export class SidePanelComponent implements AfterViewInit, OnInit {
onTabChange(event: MatTabChangeEvent) {
this.tabChange.emit(event);
this.selectedIndex = event.index;
const label = event.tab?.textLabel?.toLowerCase() || '';
if (label.includes('trace')) {
this.analyticsService.sendEvent('trace_view_toggle');
} else if (label.includes('event')) {
this.analyticsService.sendEvent('event_view_toggle');
}
}

switchToEvalTab() {
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;
}
}
}
Loading