From 742028b6910850028fade107972b356184fe28bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 29 Mar 2021 09:47:04 +0200 Subject: [PATCH] Add analytics integration (#8695) Co-authored-by: Bram Kragten Co-authored-by: Philip Allgaier --- hassio/src/system/hassio-supervisor-info.ts | 51 ++--- src/components/ha-analytics.ts | 188 ++++++++++++++++++ src/data/analytics.ts | 27 +++ src/data/onboarding.ts | 7 + src/onboarding/ha-onboarding.ts | 13 ++ src/onboarding/onboarding-analytics.ts | 124 ++++++++++++ src/panels/config/core/ha-config-analytics.ts | 132 ++++++++++++ .../config/core/ha-config-section-core.js | 2 + src/translations/en.json | 35 ++++ 9 files changed, 555 insertions(+), 24 deletions(-) create mode 100644 src/components/ha-analytics.ts create mode 100644 src/data/analytics.ts create mode 100644 src/onboarding/onboarding-analytics.ts create mode 100644 src/panels/config/core/ha-config-analytics.ts diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index 2825e2f9ba..4aaf394d5e 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -8,6 +8,7 @@ import { property, TemplateResult, } from "lit-element"; +import { atLeastVersion } from "../../../src/common/config/version"; import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-card"; @@ -150,30 +151,32 @@ class HassioSupervisorInfo extends LitElement { ${this.supervisor.supervisor.supported - ? html` - - ${this.supervisor.localize( - "system.supervisor.share_diagnostics" - )} - -
- ${this.supervisor.localize( - "system.supervisor.share_diagnostics_description" - )} - -
- -
` + ? !atLeastVersion(this.hass.config.version, 2021, 4) + ? html` + + ${this.supervisor.localize( + "system.supervisor.share_diagnostics" + )} + +
+ ${this.supervisor.localize( + "system.supervisor.share_diagnostics_description" + )} + +
+ +
` + : "" : html`
${this.supervisor.localize( "system.supervisor.unsupported_title" diff --git a/src/components/ha-analytics.ts b/src/components/ha-analytics.ts new file mode 100644 index 0000000000..d2833940c2 --- /dev/null +++ b/src/components/ha-analytics.ts @@ -0,0 +1,188 @@ +import "@polymer/paper-tooltip/paper-tooltip"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { isComponentLoaded } from "../common/config/is_component_loaded"; +import { fireEvent } from "../common/dom/fire_event"; +import { Analytics, AnalyticsPreferences } from "../data/analytics"; +import { haStyle } from "../resources/styles"; +import { HomeAssistant } from "../types"; +import { documentationUrl } from "../util/documentation-url"; +import "./ha-checkbox"; +import type { HaCheckbox } from "./ha-checkbox"; +import "./ha-settings-row"; + +const ADDITIONAL_PREFERENCES = ["usage", "statistics"]; + +declare global { + interface HASSDomEvents { + "analytics-preferences-changed": { preferences: AnalyticsPreferences }; + } +} + +@customElement("ha-analytics") +export class HaAnalytics extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public analytics!: Analytics; + + protected render(): TemplateResult { + if (!this.analytics.huuid) { + return html``; + } + + const enabled = this.analytics.preferences.base; + + return html` +

+ ${this.hass.localize( + "ui.panel.config.core.section.core.analytics.instance_id", + "huuid", + this.analytics.huuid + )} +

+ + + + + + + ${this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.base.title` + )} + + + ${this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.base.description` + )} + + + ${ADDITIONAL_PREFERENCES.map( + (preference) => + html` + + + + ${!enabled + ? html`${this.hass.localize( + "ui.panel.config.core.section.core.analytics.needs_base" + )} + ` + : ""} + + + ${preference === "usage" + ? isComponentLoaded(this.hass, "hassio") + ? this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.usage_supervisor.title` + ) + : this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.usage.title` + ) + : this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.${preference}.title` + )} + + + ${preference === "usage" + ? isComponentLoaded(this.hass, "hassio") + ? this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.usage_supervisor.description` + ) + : this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.usage.description` + ) + : this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.${preference}.description` + )} + + ` + )} + + + + + + + ${this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.diagnostics.title` + )} + + + ${this.hass.localize( + `ui.panel.config.core.section.core.analytics.preference.diagnostics.description` + )} + + +

+ ${this.hass.localize( + "ui.panel.config.core.section.core.analytics.documentation", + "link", + html` + ${documentationUrl(this.hass, "/integrations/analytics/")} + ` + )} +

+ `; + } + + private _handleRowCheckboxClick(ev: Event) { + const checkbox = ev.currentTarget as HaCheckbox; + const preference = (checkbox as any).preference; + const preferences = { ...this.analytics.preferences }; + + if (checkbox.checked) { + if (preferences[preference]) { + return; + } + preferences[preference] = true; + } else { + preferences[preference] = false; + } + + fireEvent(this, "analytics-preferences-changed", { preferences }); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + .error { + color: var(--error-color); + } + + ha-settings-row { + padding: 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-analytics": HaAnalytics; + } +} diff --git a/src/data/analytics.ts b/src/data/analytics.ts new file mode 100644 index 0000000000..d8e04f69a1 --- /dev/null +++ b/src/data/analytics.ts @@ -0,0 +1,27 @@ +import { HomeAssistant } from "../types"; + +export interface AnalyticsPreferences { + base?: boolean; + diagnostics?: boolean; + usage?: boolean; + statistics?: boolean; +} + +export interface Analytics { + preferences: AnalyticsPreferences; + huuid: string; +} + +export const getAnalyticsDetails = (hass: HomeAssistant) => + hass.callWS({ + type: "analytics", + }); + +export const setAnalyticsPreferences = ( + hass: HomeAssistant, + preferences: AnalyticsPreferences +) => + hass.callWS({ + type: "analytics/preferences", + preferences, + }); diff --git a/src/data/onboarding.ts b/src/data/onboarding.ts index 48f7e42f40..3d9e24ee9b 100644 --- a/src/data/onboarding.ts +++ b/src/data/onboarding.ts @@ -12,10 +12,14 @@ export interface OnboardingIntegrationStepResponse { auth_code: string; } +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OnboardingAnalyticsStepResponse {} + export interface OnboardingResponses { user: OnboardingUserStepResponse; core_config: OnboardingCoreConfigStepResponse; integration: OnboardingIntegrationStepResponse; + analytics: OnboardingAnalyticsStepResponse; } export type ValidOnboardingStep = keyof OnboardingResponses; @@ -49,6 +53,9 @@ export const onboardCoreConfigStep = (hass: HomeAssistant) => "onboarding/core_config" ); +export const onboardAnalyticsStep = (hass: HomeAssistant) => + hass.callApi("POST", "onboarding/analytics"); + export const onboardIntegrationStep = ( hass: HomeAssistant, params: { client_id: string; redirect_uri: string } diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index 8bd4e25210..a6177099be 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -31,6 +31,7 @@ import { HomeAssistant } from "../types"; import { registerServiceWorker } from "../util/register-service-worker"; import "./onboarding-create-user"; import "./onboarding-loading"; +import "./onboarding-analytics"; type OnboardingEvent = | { @@ -43,6 +44,9 @@ type OnboardingEvent = } | { type: "integration"; + } + | { + type: "analytics"; }; declare global { @@ -102,6 +106,15 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { > `; } + if (step.step === "analytics") { + return html` + + `; + } + if (step.step === "integration") { return html` + ${this.localize( + "ui.panel.page-onboarding.analytics.intro", + "link", + html`https://analytics.home-assistant.io` + )} +

+ + + ${this._error ? html`
${this._error}
` : ""} + + `; + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this.addEventListener("keypress", (ev) => { + if (ev.keyCode === 13) { + this._save(ev); + } + }); + this._load(); + } + + private _preferencesChanged(event: CustomEvent): void { + this._analyticsDetails = { + ...this._analyticsDetails!, + preferences: event.detail.preferences, + }; + } + + private async _save(ev) { + ev.preventDefault(); + try { + await setAnalyticsPreferences( + this.hass, + this._analyticsDetails!.preferences + ); + + await onboardAnalyticsStep(this.hass); + fireEvent(this, "onboarding-step", { + type: "analytics", + }); + } catch (err) { + alert(`Failed to save: ${err.message}`); + } + } + + private async _load() { + this._error = undefined; + try { + this._analyticsDetails = await getAnalyticsDetails(this.hass); + } catch (err) { + this._error = err.message || err; + } + } + + static get styles(): CSSResult { + return css` + .error { + color: var(--error-color); + } + + .footer { + margin-top: 16px; + text-align: right; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "onboarding-analytics": OnboardingAnalytics; + } +} diff --git a/src/panels/config/core/ha-config-analytics.ts b/src/panels/config/core/ha-config-analytics.ts new file mode 100644 index 0000000000..fae605964c --- /dev/null +++ b/src/panels/config/core/ha-config-analytics.ts @@ -0,0 +1,132 @@ +import "@material/mwc-button/mwc-button"; +import { + css, + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import "../../../components/ha-analytics"; +import "../../../components/ha-card"; +import "../../../components/ha-checkbox"; +import "../../../components/ha-settings-row"; +import { + Analytics, + getAnalyticsDetails, + setAnalyticsPreferences, +} from "../../../data/analytics"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; + +@customElement("ha-config-analytics") +class ConfigAnalytics extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private _analyticsDetails?: Analytics; + + @internalProperty() private _error?: string; + + protected render(): TemplateResult { + if ( + !isComponentLoaded(this.hass, "analytics") || + !this.hass.user?.is_owner || + !this._analyticsDetails?.huuid + ) { + return html``; + } + + return html` + +
+ ${this._error ? html`
${this._error}
` : ""} +

+ ${this.hass.localize( + "ui.panel.config.core.section.core.analytics.introduction", + "link", + html`https://analytics.home-assistant.io` + )} +

+ +
+
+ + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.save_button" + )} + +
+
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + if (isComponentLoaded(this.hass, "analytics")) { + this._load(); + } + } + + private async _load() { + this._error = undefined; + try { + this._analyticsDetails = await getAnalyticsDetails(this.hass); + } catch (err) { + this._error = err.message || err; + } + } + + private async _save() { + this._error = undefined; + try { + await setAnalyticsPreferences( + this.hass, + this._analyticsDetails?.preferences || {} + ); + } catch (err) { + this._error = err.message || err; + } + } + + private _preferencesChanged(event: CustomEvent): void { + this._analyticsDetails = { + ...this._analyticsDetails!, + preferences: event.detail.preferences, + }; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + .error { + color: var(--error-color); + } + + ha-settings-row { + padding: 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-analytics": ConfigAnalytics; + } +} diff --git a/src/panels/config/core/ha-config-section-core.js b/src/panels/config/core/ha-config-section-core.js index d91dda88f1..4e50ab56c4 100644 --- a/src/panels/config/core/ha-config-section-core.js +++ b/src/panels/config/core/ha-config-section-core.js @@ -8,6 +8,7 @@ import "../../../components/ha-card"; import LocalizeMixin from "../../../mixins/localize-mixin"; import "../../../styles/polymer-ha-style"; import "../ha-config-section"; +import "./ha-config-analytics"; import "./ha-config-core-form"; import "./ha-config-name-form"; import "./ha-config-url-form"; @@ -29,6 +30,7 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) { + `; } diff --git a/src/translations/en.json b/src/translations/en.json index d411fffc92..08383a9dcb 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -977,6 +977,35 @@ "save_button": "Save", "external_url": "External URL", "internal_url": "Internal URL" + }, + "analytics": { + "header": "Analytics", + "introduction": "Share analytics from your instance. This data will be publiclly available at {link}", + "instance_id": "Instance ID: {huuid}", + "needs_base": "You need to enable base analytics for this option to be available", + "preference": { + "base": { + "title": "Basic analytics", + "description": "This includes the instance ID, the version and the installation type" + }, + "diagnostics": { + "title": "Diagnostics", + "description": "Share crash reports and diagnostic information" + }, + "usage": { + "title": "Used integrations", + "description": "This includes the names of your integrations" + }, + "usage_supervisor": { + "title": "Used integrations and add-ons", + "description": "This includes the names and capabilities of your integrations and add-ons" + }, + "statistics": { + "title": "Usage statistics", + "description": "This includes a count of elements in your installation, for a full list look at the documentation" + } + }, + "documentation": "Before you enable this make sure you visit the analytics documentation page {link} to understand what you are sending and how it's stored." } } } @@ -3420,6 +3449,8 @@ }, "page-onboarding": { "intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?", + "next": "Next", + "finish": "Finish", "user": { "intro": "Let's get started by creating a user account.", "required_field": "Required", @@ -3449,6 +3480,10 @@ "more_integrations": "More", "finish": "Finish" }, + "analytics": { + "intro": "Share analytics from your instance. This data will be publiclly available at {link}", + "finish": "Next" + }, "restore": { "description": "Alternatively you can restore from a previous snapshot.", "in_progress": "Restore in progress",