Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Analytics opt in for posthog (#6936)
Browse files Browse the repository at this point in the history
* Add a new flag pseudonymousAnalyticsOptIn replacing analyticsOptIn, stored at account level, so people only need to opt in once.

* Show a toast in login to users that have analyticsOptIn set but not yet pseudonymousAnalyticsOptIn prompting them confirm the new method is okay. Update the copy of the existing opt-in toast. Don't notify users that previously opted out.

* Update the copy in settings

* Add a new learn more dialog

* Support a new config flag analyticsOwner which is used in these toasts when explaining which entity the data is sent to ("Help improve %(analyticsOwner)"). If unset, display brand. This allows deployments whose brand differs from the receiver of the analytics to explain the situation to their users (e.g. AcmeCorp badges their app, but explains the data is sent to Element, not them)

* The new opt-in and flags are only used when posthog is configured; prior to that there are no changes to UX or tracking behaviour.
  • Loading branch information
James Salter authored Dec 5, 2021
1 parent 961fec9 commit 5219b6b
Show file tree
Hide file tree
Showing 19 changed files with 511 additions and 149 deletions.
1 change: 1 addition & 0 deletions res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_AnalyticsLearnMoreDialog.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
Expand Down
4 changes: 4 additions & 0 deletions res/css/views/dialogs/_Analytics.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ limitations under the License.

.mx_AnalyticsModal table {
margin: 10px 0px;

.mx_AnalyticsModal_label {
width: 400px;
}
}
64 changes: 64 additions & 0 deletions res/css/views/dialogs/_AnalyticsLearnMoreDialog.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
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.
*/

.mx_AnalyticsLearnMoreDialog {
max-width: 500px;
.mx_AnalyticsLearnMore_image_holder {
background-image: url('$(res)/img/element-shiny.svg');
background-repeat: no-repeat;
background-position: center top;
height: 112px;
padding: 20px 0px;
}

.mx_Dialog_content {
margin-bottom: 0px;
}

.mx_AnalyticsLearnMore_copy {
border-bottom: 1px solid $menu-border-color;
padding-bottom: 20px;
margin-bottom: 20px;
}

a {
color: $accent;
text-decoration: none;
}

.mx_AnalyticsPolicyLink {
display: inline-block;
mask-image: url('$(res)/img/external-link.svg');
background-color: $accent;
mask-repeat: no-repeat;
mask-size: contain;
width: 12px;
height: 12px;
margin-left: 3px;
vertical-align: middle;
}

.mx_AnalyticsLearnMore_bullets {
padding-left: 0px;
}

.mx_AnalyticsLearnMore_bullets li {
background: url('$(res)/img/tick-circle.svg') no-repeat;
list-style-type: none;
padding: 2px 0px 20px 32px;
vertical-align: middle;
}
}
12 changes: 8 additions & 4 deletions res/css/views/toasts/_AnalyticsToast.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ limitations under the License.
*/

.mx_AnalyticsToast {
.mx_AccessibleButton_kind_danger {
background: none;
color: $accent;
.mx_AccessibleButton_kind_danger_outline {
background-color: $accent;
color: #ffffff;
border: 1px solid $accent;
font-weight: $font-semi-bold;
}

.mx_AccessibleButton_kind_primary {
background: $accent;
background-color: $accent;
color: #ffffff;
border: 1px solid $accent;
font-weight: $font-semi-bold;
}
}
15 changes: 15 additions & 0 deletions res/img/element-shiny.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions res/img/tick-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 12 additions & 2 deletions src/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -393,16 +393,26 @@ export class Analytics {
];

// FIXME: Using an import will result in test failures
const cookiePolicyUrl = SdkConfig.get().piwik?.policyUrl;
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
const cookiePolicyLink = _t(
"Our complete cookie policy can be found <CookiePolicyLink>here</CookiePolicyLink>.",
{},
{
"CookiePolicyLink": (sub) => {
return <a href={cookiePolicyUrl} target="_blank" rel="noreferrer noopener">{ sub }</a>;
},
});
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
title: _t('Analytics'),
description: <div className="mx_AnalyticsModal">
<div>{ _t('The information being sent to us to help make %(brand)s better includes:', {
{ cookiePolicyUrl && <p>{ cookiePolicyLink }</p> }
<div>{ _t('Some examples of the information being sent to us to help make %(brand)s better includes:', {
brand: SdkConfig.get().brand,
}) }</div>
<table>
{ rows.map((row) => <tr key={row[0]}>
<td>{ _t(
<td className="mx_AnalyticsModal_label">{ _t(
customVariables[row[0]].expl,
customVariables[row[0]].getTextVariables ?
customVariables[row[0]].getTextVariables() :
Expand Down
7 changes: 4 additions & 3 deletions src/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,12 +585,13 @@ async function doSetLoggedIn(

MatrixClientPeg.replaceUsingCreds(credentials);

PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);

setSentryUser(credentials.userId);

const client = MatrixClientPeg.get();
if (PosthogAnalytics.instance.isEnabled()) {
PosthogAnalytics.instance.startListeningToSettingsChanges();
}

const client = MatrixClientPeg.get();
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
// If we just logged in, try to rehydrate a device instead of using a
// new device. If it succeeds, we'll get a new device ID, so make sure
Expand Down
63 changes: 30 additions & 33 deletions src/PosthogAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ limitations under the License.
import posthog, { PostHog } from 'posthog-js';
import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig';
import SettingsStore from './settings/SettingsStore';
import { MatrixClientPeg } from "./MatrixClientPeg";
import { MatrixClient } from "matrix-js-sdk/src/client";

import { logger } from "matrix-js-sdk/src/logger";
import SettingsStore from "./settings/SettingsStore";

/* Posthog analytics tracking.
*
Expand Down Expand Up @@ -132,10 +132,10 @@ export class PosthogAnalytics {

private anonymity = Anonymity.Disabled;
// set true during the constructor if posthog config is present, otherwise false
private enabled = false;
private readonly enabled: boolean = false;
private static _instance = null;
private platformSuperProperties = {};
private static ANALYTICS_ID_EVENT_TYPE = "im.vector.web.analytics_id";
private static ANALYTICS_EVENT_TYPE = "im.vector.analytics";

public static get instance(): PosthogAnalytics {
if (!this._instance) {
Expand Down Expand Up @@ -197,29 +197,6 @@ export class PosthogAnalytics {
return properties;
};

private static getAnonymityFromSettings(): Anonymity {
// determine the current anonymity level based on current user settings

// "Send anonymous usage data which helps us improve Element. This will use a cookie."
const analyticsOptIn = SettingsStore.getValue("analyticsOptIn", null, true);

// (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie."
//
// TODO: Currently, this is only a labs flag, for testing purposes.
const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymous_analytics_opt_in", null, true);

let anonymity;
if (pseudonumousOptIn) {
anonymity = Anonymity.Pseudonymous;
} else if (analyticsOptIn) {
anonymity = Anonymity.Anonymous;
} else {
anonymity = Anonymity.Disabled;
}

return anonymity;
}

private registerSuperProperties(properties: posthog.Properties) {
if (this.enabled) {
this.posthog.register(properties);
Expand Down Expand Up @@ -279,7 +256,7 @@ export class PosthogAnalytics {
// Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
// different devices to send the same ID.
try {
const accountData = await client.getAccountDataFromServer(PosthogAnalytics.ANALYTICS_ID_EVENT_TYPE);
const accountData = await client.getAccountDataFromServer(PosthogAnalytics.ANALYTICS_EVENT_TYPE);
let analyticsID = accountData?.id;
if (!analyticsID) {
// Couldn't retrieve an analytics ID from user settings, so create one and set it on the server.
Expand All @@ -288,7 +265,8 @@ export class PosthogAnalytics {
// until the next time account data is refreshed and this function is called (most likely on next
// page load). This will happen pretty infrequently, so we can tolerate the possibility.
analyticsID = analyticsIdGenerator();
await client.setAccountData("im.vector.web.analytics_id", { id: analyticsID });
await client.setAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE,
Object.assign({ id: analyticsID }, accountData));
}
this.posthog.identify(analyticsID);
} catch (e) {
Expand All @@ -307,7 +285,7 @@ export class PosthogAnalytics {
if (this.enabled) {
this.posthog.reset();
}
this.setAnonymity(Anonymity.Anonymous);
this.setAnonymity(Anonymity.Disabled);
}

public async trackPseudonymousEvent<E extends IPseudonymousEvent>(
Expand Down Expand Up @@ -351,12 +329,31 @@ export class PosthogAnalytics {
this.registerSuperProperties(this.platformSuperProperties);
}

public async updateAnonymityFromSettings(userId?: string): Promise<void> {
public async updateAnonymityFromSettings(pseudonymousOptIn: boolean): Promise<void> {
// Update this.anonymity based on the user's analytics opt-in settings
// Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
const anonymity = pseudonymousOptIn ? Anonymity.Pseudonymous : Anonymity.Disabled;
this.setAnonymity(anonymity);
if (anonymity === Anonymity.Pseudonymous) {
await this.identifyUser(MatrixClientPeg.get(), PosthogAnalytics.getRandomAnalyticsId);
}

if (anonymity !== Anonymity.Disabled) {
await PosthogAnalytics.instance.updatePlatformSuperProperties();
}
}

public startListeningToSettingsChanges(): void {
// Listen to account data changes from sync so we can observe changes to relevant flags and update.
// This is called -
// * On page load, when the account data is first received by sync
// * On login
// * When another device changes account data
// * When the user changes their preferences on this device
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
SettingsStore.watchSetting("pseudonymousAnalyticsOptIn", null,
(originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => {
this.updateAnonymityFromSettings(!!newValue);
});
}
}
Loading

0 comments on commit 5219b6b

Please sign in to comment.