diff --git a/package.json b/package.json index ffa0a5c10c..feca4bd44b 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@wireapp/api-client": "27.10.1", "@wireapp/commons": "5.3.0", "@wireapp/react-ui-kit": "9.26.3", + "@wireapp/telemetry": "0.1.2", "core-js": "3.39.0", "dotenv": "16.4.5", "dotenv-extended": "2.9.0", @@ -14,7 +15,8 @@ "react-dom": "18.3.1", "react-i18next": "11.18.6", "react-router": "7.0.1", - "react-router-dom": "6.28.0" + "react-router-dom": "6.28.0", + "uuid": "11.0.3" }, "devDependencies": { "@babel/core": "7.26.0", @@ -31,6 +33,7 @@ "@types/react": "18.3.12", "@types/react-dom": "18.3.1", "@types/react-router-dom": "5.3.3", + "@types/uuid": "10.0.0", "@types/webpack-env": "1.18.5", "@typescript-eslint/eslint-plugin": "8.15.0", "@typescript-eslint/parser": "7.17.0", diff --git a/server/Server.ts b/server/Server.ts index b17993e8e5..04c660594a 100644 --- a/server/Server.ts +++ b/server/Server.ts @@ -191,6 +191,7 @@ class Server { this.app.get('/favicon.ico', (_req, res) => res.sendFile(path.join(__dirname, 'img', 'favicon.ico'))); this.app.get('/robots.txt', (_req, res) => res.sendFile(path.join(__dirname, 'robots', 'robots.txt'))); this.app.use('/script', express.static(path.join(__dirname, 'static', 'script'))); + this.app.use('/libs', express.static(path.join(__dirname, 'libs'))); } initTemplateEngine(): void { diff --git a/server/ServerConfig.ts b/server/ServerConfig.ts index abc0eebe8f..82083e46ab 100644 --- a/server/ServerConfig.ts +++ b/server/ServerConfig.ts @@ -48,6 +48,8 @@ export interface ServerConfig { URL_TERMS_OF_USE_TEAMS: string; URL_SUPPORT_BACKUP_HISTORY: string; }; + COUNTLY_SERVER_URL: string; + COUNTLY_API_KEY: string; VERSION: string; }; COMMIT: string; diff --git a/server/bin/copy_server_assets.js b/server/bin/copy_server_assets.js index f7591357ec..0ed104fdab 100755 --- a/server/bin/copy_server_assets.js +++ b/server/bin/copy_server_assets.js @@ -24,9 +24,15 @@ const path = require('path'); const srcFolder = '../'; const distFolder = '../dist/'; +const npmModulesFolder = '../../node_modules/'; const assetFolders = ['.ebextensions/', 'img/', 'robots/', 'templates/', 'certificate']; assetFolders.forEach(assetFolder => { fs.copySync(path.resolve(__dirname, srcFolder, assetFolder), path.resolve(__dirname, distFolder, assetFolder)); }); + +fs.copySync( + path.resolve(__dirname, npmModulesFolder, '@wireapp/telemetry/lib/embed.js'), + path.resolve(__dirname, distFolder, 'libs/wire/telemetry/embed.js'), +); diff --git a/server/config.ts b/server/config.ts index cc6928f09d..fc97b326a4 100644 --- a/server/config.ts +++ b/server/config.ts @@ -134,6 +134,8 @@ const config: ServerConfig = { URL_SUPPORT_BACKUP_HISTORY: process.env.URL_SUPPORT_BACKUP_HISTORY, }, VERSION: readFile(VERSION_FILE, '0.0.0'), + COUNTLY_SERVER_URL: process.env.COUNTLY_SERVER_URL, + COUNTLY_API_KEY: process.env.COUNTLY_API_KEY, }, COMMIT: readFile(COMMIT_FILE, ''), PIWIK_HOSTNAME: process.env.PIWIK_HOSTNAME, diff --git a/server/templates/index.hbs b/server/templates/index.hbs index 259601c82e..128a8e58b9 100644 --- a/server/templates/index.hbs +++ b/server/templates/index.hbs @@ -17,7 +17,9 @@ - + {{#if config.CLIENT.COUNTLY_API_KEY}} + + {{/if}} Wire diff --git a/src/script/Environment.ts b/src/script/Environment.ts index 4f77dc892d..a3d8cb476f 100644 --- a/src/script/Environment.ts +++ b/src/script/Environment.ts @@ -33,6 +33,8 @@ declare global { NEW_PASSWORD_MINIMUM_LENGTH: number; RAYGUN_API_KEY: string; STRIPE_API_KEY: string; + COUNTLY_SERVER_URL: string; + COUNTLY_API_KEY: string; URL: { ACCOUNT_DELETE_SURVEY: string; DOWNLOAD_ANDROID_BASE: string; @@ -99,3 +101,5 @@ export const TEAMS_URL = window.wire.env.URL.TEAMS_BASE; export const WEBSITE_URL = window.wire.env.URL.WEBSITE_BASE; export const URL_SUPPORT_BACKUP_HISTORY = window.wire.env.URL.URL_SUPPORT_BACKUP_HISTORY; export const URL_TERMS_OF_USE_TEAMS = window.wire.env.URL.URL_TERMS_OF_USE_TEAMS; +export const COUNTLY_SERVER_URL = window.wire.env.COUNTLY_SERVER_URL; +export const COUNTLY_API_KEY = window.wire.env.COUNTLY_API_KEY; diff --git a/src/script/Root.tsx b/src/script/Root.tsx index da97b81593..7c05e345ec 100644 --- a/src/script/Root.tsx +++ b/src/script/Root.tsx @@ -27,6 +27,7 @@ import {TermsAcknowledgement} from './page/migration/TermsAcknowledgement'; import {ConfirmInvitation} from './page/migration/ConfirmInvitation'; import {Welcome} from './page/migration/Welcome'; import {AcceptInvitation} from './page/migration/AcceptInvitation'; +import {initializeTelemetry} from './util/Tracking/Tracking'; const LazyIndex = lazy(() => import('./page/Index')); const LazyDeleteAccount = lazy(() => import('./page/DeleteAccount')); @@ -45,6 +46,8 @@ const Root = () => { const hlParam = queryParams.get(QUERY_KEY.LANG); const userLocale = navigator.languages?.length ? navigator.languages[0] : navigator.language; + initializeTelemetry(); + if (!hlParam && !userLocale.includes('en')) { queryParams.set(QUERY_KEY.LANG, userLocale); window.history.pushState(null, '', `?${queryParams.toString()}`); diff --git a/src/script/page/migration/AcceptInvitation.tsx b/src/script/page/migration/AcceptInvitation.tsx index 8d596a7cd6..85ce2394e2 100644 --- a/src/script/page/migration/AcceptInvitation.tsx +++ b/src/script/page/migration/AcceptInvitation.tsx @@ -40,6 +40,8 @@ import {useTranslation} from 'react-i18next'; import {LoginData} from '@wireapp/api-client/lib/auth'; import {ClientType} from '@wireapp/api-client/lib/client'; import {useActionContext} from 'script/module/action'; +import {reportEvent} from 'script/util/Tracking/Tracking'; +import {EventName, SegmentationKey, SegmentationValue} from 'script/util/Tracking/types'; export const AcceptInvitation = () => { const [searchParams] = useSearchParams(); @@ -54,7 +56,15 @@ export const AcceptInvitation = () => { const code = searchParams.get(QUERY_KEY.TEAM_CODE); const cachedCode = getTeamInvitationCode(); + const trackEvent = (step: SegmentationValue) => { + reportEvent(EventName.USER_MIGRATION_LOGIN, { + [SegmentationKey.STEP]: step, + }); + }; + useEffect(() => { + trackEvent(SegmentationValue.OPENED); + if (!code && !cachedCode) { navigate(ROUTE.HOME); } @@ -75,6 +85,7 @@ export const AcceptInvitation = () => { const handleLogin = async (event: FormEvent) => { event.preventDefault(); setError(''); + trackEvent(SegmentationValue.CONTINUE_CLICKED); const login: LoginData = { clientType: ClientType.PERMANENT, @@ -116,6 +127,7 @@ export const AcceptInvitation = () => { title={t('invitationPagLoginLabel')} value={email} data-uie-name="enter-login-identifier" + onBlur={() => trackEvent(SegmentationValue.EMAIL_ENTERED)} /> { type="password" value={password} data-uie-name="enter-login-password" + onBlur={() => trackEvent(SegmentationValue.PASSWORD_ENTERED)} />
- + trackEvent(SegmentationValue.PASSWORD_FORGOTTEN)} + data-uie-name="go-forgot-password" + > {t('forgotPassword')}
diff --git a/src/script/page/migration/ConfirmInvitation.tsx b/src/script/page/migration/ConfirmInvitation.tsx index 4b35187e84..e617668dad 100644 --- a/src/script/page/migration/ConfirmInvitation.tsx +++ b/src/script/page/migration/ConfirmInvitation.tsx @@ -30,12 +30,14 @@ import { useMatchMedia, } from '@wireapp/react-ui-kit'; import {loginContainerCss, loginSubHeaderCss, headerCss, forgotPasswordCss, buttonCss} from './styles'; -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {ROUTE} from 'script/route'; import {useNavigate} from 'react-router-dom'; import {getTeamInvitationCode, removeTeamInvitationCode} from './utils'; import {useTranslation} from 'react-i18next'; import {useActionContext} from 'script/module/action'; +import {reportEvent} from 'script/util/Tracking/Tracking'; +import {EventName, SegmentationKey, SegmentationValue} from 'script/util/Tracking/types'; export const ConfirmInvitation = () => { const isTablet = useMatchMedia(QUERY[QueryKeys.TABLET_DOWN]); @@ -48,8 +50,14 @@ export const ConfirmInvitation = () => { const [loading, setLoading] = useState(false); const code = getTeamInvitationCode(); + const trackEvent = (step: SegmentationValue) => { + reportEvent(EventName.USER_MIGRATION_CONFIRMATION, { + [SegmentationKey.STEP]: step, + }); + }; const handleSubmit = (event: any) => { event.preventDefault(); + trackEvent(SegmentationValue.CONTINUE_CLICKED); setLoading(true); teamAction .acceptInvitation({ @@ -68,6 +76,10 @@ export const ConfirmInvitation = () => { }); }; + useEffect(() => { + trackEvent(SegmentationValue.OPENED); + }, []); + return (
{isTablet && } @@ -86,6 +98,7 @@ export const ConfirmInvitation = () => { type="password" value={password} data-uie-name="enter-login-password" + onBlur={() => trackEvent(SegmentationValue.PASSWORD_ENTERED)} />
diff --git a/src/script/page/migration/TermsAcknowledgement.tsx b/src/script/page/migration/TermsAcknowledgement.tsx index a36f26b787..836ebcdea1 100644 --- a/src/script/page/migration/TermsAcknowledgement.tsx +++ b/src/script/page/migration/TermsAcknowledgement.tsx @@ -54,6 +54,8 @@ import {useTranslation} from 'react-i18next'; import MarkupTranslation from 'script/component/MarkupTranslation'; import {useActionContext} from 'script/module/action'; import {getTeamInvitationCode} from './utils'; +import {reportEvent} from 'script/util/Tracking/Tracking'; +import {EventName, SegmentationKey, SegmentationValue} from 'script/util/Tracking/types'; export const TermsAcknowledgement = () => { const navigate = useNavigate(); @@ -66,7 +68,14 @@ export const TermsAcknowledgement = () => { const [isTermOfUseAccepted, setIsTermOfUseAccepted] = useState(false); const [inviterEmail, setInviterEmail] = useState(''); + const trackEvent = (step: SegmentationValue) => { + reportEvent(EventName.USER_MIGRATION_TERMS_ACKNOWLEDGEMENT, { + [SegmentationKey.STEP]: step, + }); + }; + useEffect(() => { + trackEvent(SegmentationValue.OPENED); teamAction .getInvitationInfo(code) .then(res => { @@ -83,6 +92,11 @@ export const TermsAcknowledgement = () => { ); } + const handleSubmit = () => { + navigate(ROUTE.CONFIRM_INVITATION); + trackEvent(SegmentationValue.CONTINUE_CLICKED); + }; + return (
{isTablet && ( @@ -141,6 +155,7 @@ export const TermsAcknowledgement = () => { checked={isMigrationAccepted} onChange={(event: React.ChangeEvent) => { setIsMigrationAccepted(event.target.checked); + trackEvent(SegmentationValue.AGREE_MIGRATION_TERMS_CHECK); }} id="do-accept-migration" data-uie-name="do-accept-migration" @@ -152,6 +167,7 @@ export const TermsAcknowledgement = () => { checked={isTermOfUseAccepted} onChange={(event: React.ChangeEvent) => { setIsTermOfUseAccepted(event.target.checked); + trackEvent(SegmentationValue.AGREE_TOC_CHECK); }} id="do-accept-terms" data-uie-name="do-accept-terms" @@ -166,7 +182,7 @@ export const TermsAcknowledgement = () => {
{isOwner(selfMember.permissions) && ( @@ -118,7 +132,7 @@ export const Welcome = () => { data-uie-name="do-go-to-team-management" block variant={ButtonVariant.SECONDARY} - onClick={() => secureOpen(EXTERNAL_ROUTE.TEAM_SETTINGS)} + onClick={handleTMOpen} > {t('welcomePageTMOpenText')} diff --git a/src/script/util/Tracking/Tracking.ts b/src/script/util/Tracking/Tracking.ts new file mode 100644 index 0000000000..43c3adaa36 --- /dev/null +++ b/src/script/util/Tracking/Tracking.ts @@ -0,0 +1,90 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {APP_NAME, COUNTLY_API_KEY, COUNTLY_SERVER_URL, VERSION} from 'script/Environment'; +import * as telemetry from '@wireapp/telemetry'; +import {v4 as createUUID} from 'uuid'; +import {EventName, SegmentationKey} from './types'; +import {REPORTING_DEVICE_ID} from './constants'; + +let isProductReportingActivated = false; + +const getDeviceId = () => { + const deviceIdFromStorage = window.localStorage.getItem(REPORTING_DEVICE_ID); + + if (!deviceIdFromStorage) { + const newDeviceId = createUUID(); + window.localStorage.setItem(REPORTING_DEVICE_ID, newDeviceId); + return newDeviceId; + } + + return deviceIdFromStorage; +}; + +export const initializeTelemetry = () => { + if (!COUNTLY_SERVER_URL || !COUNTLY_API_KEY) { + return; + } + + if (!telemetry.isLoaded() || isProductReportingActivated) { + return; + } + + isProductReportingActivated = true; + + telemetry.initialize({ + appVersion: VERSION, + provider: { + apiKey: COUNTLY_API_KEY, + serverUrl: COUNTLY_SERVER_URL, + enableLogging: false, + autoClickTracking: true, + }, + }); + + telemetry.addAllConsentFeatures(); + + const deviceId = getDeviceId(); + + telemetry.changeDeviceId(deviceId); + telemetry.disableOfflineMode(deviceId); + + telemetry.beginSession(); + + window.addEventListener('beforeunload', () => { + telemetry.endSession(); + }); +}; + +export const reportEvent = (eventName: EventName, segmentation?: Record) => { + if (!isProductReportingActivated) { + return; + } + + const telemetryEvent: telemetry.TelemetryEvent = { + name: eventName, + segmentation: { + ...segmentation, + [SegmentationKey.APP]: APP_NAME, + [SegmentationKey.APP_VERSION]: VERSION, + }, + }; + + telemetry.trackEvent(telemetryEvent); +}; diff --git a/src/script/util/Tracking/constants.ts b/src/script/util/Tracking/constants.ts new file mode 100644 index 0000000000..f03ed616c6 --- /dev/null +++ b/src/script/util/Tracking/constants.ts @@ -0,0 +1,21 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export const REPORTING_DEVICE_ID = 'REPORTING_DEVICE_ID'; +export const APP_NAME = 'account'; diff --git a/src/script/util/Tracking/types.ts b/src/script/util/Tracking/types.ts new file mode 100644 index 0000000000..fecb946ff5 --- /dev/null +++ b/src/script/util/Tracking/types.ts @@ -0,0 +1,43 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +export enum EventName { + USER_MIGRATION_LOGIN = 'user.migration_login', + USER_MIGRATION_CONFIRMATION = 'user.migration_confirmation', + USER_MIGRATION_TERMS_ACKNOWLEDGEMENT = 'user.migration_terms_acknowledgement', + USER_MIGRATION_WELCOME = 'user.migration_welcome', +} + +export enum SegmentationKey { + APP = 'app', + APP_VERSION = 'app_version', + STEP = 'step', +} + +export enum SegmentationValue { + OPENED = 'opened', + EMAIL_ENTERED = 'email_entered', + PASSWORD_ENTERED = 'password_entered', + CONTINUE_CLICKED = 'continue_clicked', + PASSWORD_FORGOTTEN = 'password_forgotten', + AGREE_MIGRATION_TERMS_CHECK = 'agree_migration_terms_check', + AGREE_TOC_CHECK = 'agree_toc_checked', + OPENED_WEB_APP = 'opened_web_app', + OPENED_TM = 'opened_tm', +} diff --git a/yarn.lock b/yarn.lock index 7981216af9..2c09835211 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3685,6 +3685,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:10.0.0": + version: 10.0.0 + resolution: "@types/uuid@npm:10.0.0" + checksum: e3958f8b0fe551c86c14431f5940c3470127293280830684154b91dc7eb3514aeb79fe3216968833cf79d4d1c67f580f054b5be2cd562bebf4f728913e73e944 + languageName: node + linkType: hard + "@types/webpack-env@npm:1.18.5": version: 1.18.5 resolution: "@types/webpack-env@npm:1.18.5" @@ -4235,6 +4242,15 @@ __metadata: languageName: node linkType: hard +"@wireapp/telemetry@npm:0.1.2": + version: 0.1.2 + resolution: "@wireapp/telemetry@npm:0.1.2" + dependencies: + countly-sdk-web: 24.4.1 + checksum: 7b68979acc3d25a9ff80b2f043f9a9d58f189ea26a016a099083626f0bfb5973c1ced28d171df85cd75e85b195404046953015778110563ee234c7cda738791d + languageName: node + linkType: hard + "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -5797,6 +5813,13 @@ __metadata: languageName: node linkType: hard +"countly-sdk-web@npm:24.4.1": + version: 24.4.1 + resolution: "countly-sdk-web@npm:24.4.1" + checksum: f2c358790b11a7cdf4f438cbe0555120a8d65026032538fc592606ccb6c14d1ed30eb66323331338e7b81850d8ccd40ecacb0f20d7b03dd78ae746088985ad36 + languageName: node + linkType: hard + "create-jest@npm:^29.7.0": version: 29.7.0 resolution: "create-jest@npm:29.7.0" @@ -13750,6 +13773,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:11.0.3": + version: 11.0.3 + resolution: "uuid@npm:11.0.3" + bin: + uuid: dist/esm/bin/uuid + checksum: 646181c77e8b8df9bd07254faa703943e1c4d5ccde7d080312edf12f443f6c5750801fd9b27bf2e628594182165e6b1b880c0382538f7eca00b26622203741dc + languageName: node + linkType: hard + "v8-to-istanbul@npm:^9.0.1": version: 9.2.0 resolution: "v8-to-istanbul@npm:9.2.0" @@ -14204,6 +14236,7 @@ __metadata: "@types/react": 18.3.12 "@types/react-dom": 18.3.1 "@types/react-router-dom": 5.3.3 + "@types/uuid": 10.0.0 "@types/webpack-env": 1.18.5 "@typescript-eslint/eslint-plugin": 8.15.0 "@typescript-eslint/parser": 7.17.0 @@ -14212,6 +14245,7 @@ __metadata: "@wireapp/copy-config": 2.2.10 "@wireapp/eslint-config": 1.4.0 "@wireapp/react-ui-kit": 9.26.3 + "@wireapp/telemetry": 0.1.2 adm-zip: 0.5.16 babel-eslint: 10.1.0 babel-loader: 8.2.5 @@ -14256,6 +14290,7 @@ __metadata: terser-webpack-plugin: 5.3.10 ts-jest: 29.2.5 typescript: 5.6.3 + uuid: 11.0.3 webpack: 5.96.1 webpack-bundle-analyzer: 4.10.2 webpack-cli: 5.1.4