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 = () => {