diff --git a/ui/src/core/analytics_impl.ts b/ui/src/core/analytics_impl.ts index 0d589a3c4b..c297ed8fa2 100644 --- a/ui/src/core/analytics_impl.ts +++ b/ui/src/core/analytics_impl.ts @@ -18,7 +18,6 @@ import {VERSION} from '../gen/perfetto_version'; import {Router} from './router'; import {Analytics, TraceCategories} from '../public/analytics'; -const ANALYTICS_ID = 'G-BD89KT2P3C'; const PAGE_TITLE = 'no-page-title'; function isValidUrl(s: string) { @@ -69,21 +68,15 @@ export function initAnalytics( testingMode: boolean, embeddedMode: boolean, enable: boolean, + analyticsId: string | undefined, ): AnalyticsInternal { - // Only initialize logging on the official site and on localhost (to catch - // analytics bugs when testing locally). - // Skip analytics is the fragment has "testing=1", this is used by UI tests. + // Skip analytics if the fragment has "testing=1", this is used by UI tests. // Skip analytics in embeddedMode since iFrames do not have the same access to // local storage. // Skip analytics if the user has disabled analytics. - if ( - (window.location.origin.startsWith('http://localhost:') || - window.location.origin.endsWith('.perfetto.dev')) && - !testingMode && - !embeddedMode && - enable - ) { - return new AnalyticsImpl(); + // Skip analytics if the embedder does not provide an analytics ID. + if (analyticsId !== undefined && !testingMode && !embeddedMode && enable) { + return new AnalyticsImpl(analyticsId); } return new NullAnalytics(); } @@ -105,8 +98,10 @@ class NullAnalytics implements AnalyticsInternal { class AnalyticsImpl implements AnalyticsInternal { private initialized_ = false; + private readonly analyticsId: string; - constructor() { + constructor(analyticsId: string) { + this.analyticsId = analyticsId; // The code below is taken from the official Google Analytics docs [1] and // adapted to TypeScript. We have it here rather than as an inline script // in index.html (as suggested by GA's docs) because inline scripts don't @@ -133,7 +128,8 @@ class AnalyticsImpl implements AnalyticsInternal { if (this.initialized_) return; this.initialized_ = true; const script = document.createElement('script'); - script.src = 'https://www.googletagmanager.com/gtag/js?id=' + ANALYTICS_ID; + script.src = + 'https://www.googletagmanager.com/gtag/js?id=' + this.analyticsId; script.defer = true; document.head.appendChild(script); const route = window.location.href; @@ -144,7 +140,7 @@ class AnalyticsImpl implements AnalyticsInternal { // GA's recommendation for SPAs is to disable automatic page views and // manually send page_view events. See: // https://developers.google.com/analytics/devguides/collection/gtagjs/pages#manual_pageviews - gtagGlobals.gtag('config', ANALYTICS_ID, { + gtagGlobals.gtag('config', this.analyticsId, { allow_google_signals: false, anonymize_ip: true, page_location: route, diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts index b062dae578..852907347a 100644 --- a/ui/src/core/app_impl.ts +++ b/ui/src/core/app_impl.ts @@ -41,6 +41,8 @@ import {SerializedAppState} from './state_serialization_schema'; import {TraceImpl} from './trace_impl'; import {TraceArrayBufferSource, TraceSource} from './trace_source'; import {TaskTrackerImpl} from '../frontend/task_tracker/task_tracker'; +import {Embedder} from './embedder/embedder'; +import {createEmbedder} from './embedder/create_embedder'; export type OpenTraceArrayBufArgs = Omit< Omit, @@ -89,6 +91,7 @@ export class AppImpl implements App { readonly testingMode: boolean; readonly openTraceAsyncLimiter = new AsyncLimiter(); readonly settings: SettingsManagerImpl; + readonly embedder: Embedder; // The current active trace (if any). private _activeTrace: TraceImpl | undefined; @@ -147,10 +150,12 @@ export class AppImpl implements App { disabled: this.embeddedMode, hidden: this.initialRouteArgs.hideSidebar, }); + this.embedder = createEmbedder(); this.analytics = initAnalytics( this.testingMode, this.embeddedMode, initArgs.analyticsSetting.get(), + this.embedder.analyticsId, ); this.pages = new PageManagerImpl(this.analytics); } diff --git a/ui/src/core/embedder/create_embedder.ts b/ui/src/core/embedder/create_embedder.ts new file mode 100644 index 0000000000..87621278ab --- /dev/null +++ b/ui/src/core/embedder/create_embedder.ts @@ -0,0 +1,34 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// 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 {Embedder} from './embedder'; +import {DefaultEmbedder} from './default_embedder'; +import {PerfettoUiEmbedder} from './perfetto_ui_embedder'; + +/** + * Returns the appropriate Embedder based on the current origin. + * Uses PerfettoUiEmbedder when running on ui.perfetto.dev or localhost, + * and DefaultEmbedder otherwise. + */ +export function createEmbedder(): Embedder { + const origin = self.location?.origin ?? ''; + if ( + origin.endsWith('.perfetto.dev') || + origin.startsWith('http://localhost:') || + origin.startsWith('http://127.0.0.1:') + ) { + return new PerfettoUiEmbedder(); + } + return new DefaultEmbedder(); +} diff --git a/ui/src/core/embedder/default_embedder.ts b/ui/src/core/embedder/default_embedder.ts new file mode 100644 index 0000000000..9246662967 --- /dev/null +++ b/ui/src/core/embedder/default_embedder.ts @@ -0,0 +1,20 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// 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 {Embedder} from './embedder'; + +/** Default embedder implementation for third-party embeddings. */ +export class DefaultEmbedder implements Embedder { + readonly analyticsId = undefined; +} diff --git a/ui/src/core/embedder/embedder.ts b/ui/src/core/embedder/embedder.ts new file mode 100644 index 0000000000..d22c03d414 --- /dev/null +++ b/ui/src/core/embedder/embedder.ts @@ -0,0 +1,24 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// 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. + +/** + * Interface for embedder-specific behavior. Different implementations allow + * the UI to adapt to the environment it's running in (e.g. ui.perfetto.dev + * vs a third-party embedding). + */ +export interface Embedder { + // Returns the Google Analytics measurement ID, or undefined if analytics + // should not be enabled for this embedder. + readonly analyticsId: string | undefined; +} diff --git a/ui/src/core/embedder/perfetto_ui_embedder.ts b/ui/src/core/embedder/perfetto_ui_embedder.ts new file mode 100644 index 0000000000..79e77f17fc --- /dev/null +++ b/ui/src/core/embedder/perfetto_ui_embedder.ts @@ -0,0 +1,20 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// 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 {Embedder} from './embedder'; + +/** Embedder implementation for ui.perfetto.dev and localhost development. */ +export class PerfettoUiEmbedder implements Embedder { + readonly analyticsId = 'G-BD89KT2P3C'; +}