Add posthog Telemetry (Anonymity Logic + call duration telemetry) (#658)
Co-authored-by: Timo K <timok@element.io> Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
parent
d5326ed9ee
commit
72503d0335
14 changed files with 574 additions and 7 deletions
|
|
@ -51,6 +51,7 @@
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"postcss-preset-env": "^7",
|
"postcss-preset-env": "^7",
|
||||||
|
"posthog-js": "^1.29.0",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "18",
|
"react": "18",
|
||||||
"react-dom": "18",
|
"react-dom": "18",
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import {
|
||||||
fallbackICEServerAllowed,
|
fallbackICEServerAllowed,
|
||||||
} from "./matrix-utils";
|
} from "./matrix-utils";
|
||||||
import { widget } from "./widget";
|
import { widget } from "./widget";
|
||||||
|
import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics";
|
||||||
import { translatedError } from "./TranslatedError";
|
import { translatedError } from "./TranslatedError";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -114,7 +115,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
if (widget) {
|
if (widget) {
|
||||||
// We're inside a widget, so let's engage *matryoshka mode*
|
// We're inside a widget, so let's engage *matryoshka mode*
|
||||||
logger.log("Using a matryoshka client");
|
logger.log("Using a matryoshka client");
|
||||||
|
PosthogAnalytics.instance.setRegistrationType(
|
||||||
|
RegistrationType.Registered
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
client: await widget.client,
|
client: await widget.client,
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
|
|
@ -132,6 +135,11 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
session;
|
session;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
PosthogAnalytics.instance.setRegistrationType(
|
||||||
|
passwordlessUser
|
||||||
|
? RegistrationType.Guest
|
||||||
|
: RegistrationType.Registered
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
client: await initClient(
|
client: await initClient(
|
||||||
{
|
{
|
||||||
|
|
@ -279,6 +287,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
error: undefined,
|
error: undefined,
|
||||||
});
|
});
|
||||||
history.push("/");
|
history.push("/");
|
||||||
|
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
|
||||||
}, [history, client]);
|
}, [history, client]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
|
||||||
336
src/PosthogAnalytics.ts
Normal file
336
src/PosthogAnalytics.ts
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import posthog, { CaptureOptions, PostHog, Properties } from "posthog-js";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { widget } from "./widget";
|
||||||
|
import { settingsBus } from "./settings/useSetting";
|
||||||
|
import {
|
||||||
|
CallEndedTracker,
|
||||||
|
CallStartedTracker,
|
||||||
|
LoginTracker,
|
||||||
|
SignupTracker,
|
||||||
|
MuteCameraTracker,
|
||||||
|
MuteMicrophoneTracker,
|
||||||
|
} from "./PosthogEvents";
|
||||||
|
import { Config } from "./config/Config";
|
||||||
|
|
||||||
|
/* Posthog analytics tracking.
|
||||||
|
*
|
||||||
|
* Anonymity behaviour is as follows:
|
||||||
|
*
|
||||||
|
* - If Posthog isn't configured in `config.json`, events are not sent.
|
||||||
|
* - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is
|
||||||
|
* enabled, events are not sent (this detection is built into posthog and turned on via the
|
||||||
|
* `respect_dnt` flag being passed to `posthog.init`).
|
||||||
|
* - If the posthog analytics are explicitly activated by the user in the element call settings,
|
||||||
|
* a randomised analytics ID is created and stored in account_data for that user (shared between devices)
|
||||||
|
* so that the user can be identified in posthog.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IPosthogEvent {
|
||||||
|
// The event name that will be used by PostHog. Event names should use camelCase.
|
||||||
|
eventName: string;
|
||||||
|
|
||||||
|
// do not allow these to be sent manually, we enqueue them all for caching purposes
|
||||||
|
$set?: void;
|
||||||
|
$set_once?: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPostHogEventOptions {
|
||||||
|
timestamp?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Anonymity {
|
||||||
|
Disabled,
|
||||||
|
Anonymous,
|
||||||
|
Pseudonymous,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RegistrationType {
|
||||||
|
Guest,
|
||||||
|
Registered,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlatformProperties {
|
||||||
|
appVersion: string;
|
||||||
|
matrixBackend: "embedded" | "jssdk";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PosthogSettings {
|
||||||
|
project_api_key?: string;
|
||||||
|
api_host?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PosthogAnalytics {
|
||||||
|
/* Wrapper for Posthog analytics.
|
||||||
|
* 3 modes of anonymity are supported, governed by this.anonymity
|
||||||
|
* - Anonymity.Disabled means *no data* is passed to posthog
|
||||||
|
* - Anonymity.Anonymous means no identifier is passed to posthog
|
||||||
|
* - Anonymity.Pseudonymous means an analytics ID stored in account_data and shared between devices
|
||||||
|
* is passed to posthog.
|
||||||
|
*
|
||||||
|
* To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
|
||||||
|
*
|
||||||
|
* To pass an event to Posthog:
|
||||||
|
*
|
||||||
|
* 1. Declare a type for the event, extending IPosthogEvent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static ANALYTICS_EVENT_TYPE = "im.vector.analytics";
|
||||||
|
|
||||||
|
// set true during the constructor if posthog config is present, otherwise false
|
||||||
|
private static internalInstance = null;
|
||||||
|
|
||||||
|
private readonly enabled: boolean = false;
|
||||||
|
private anonymity = Anonymity.Pseudonymous;
|
||||||
|
private platformSuperProperties = {};
|
||||||
|
private registrationType: RegistrationType = RegistrationType.Guest;
|
||||||
|
|
||||||
|
public static get instance(): PosthogAnalytics {
|
||||||
|
if (!this.internalInstance) {
|
||||||
|
this.internalInstance = new PosthogAnalytics(posthog);
|
||||||
|
}
|
||||||
|
return this.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly posthog: PostHog) {
|
||||||
|
const posthogConfig: PosthogSettings = {
|
||||||
|
project_api_key: Config.instance.config.posthog?.api_key,
|
||||||
|
api_host: Config.instance.config.posthog?.api_host,
|
||||||
|
};
|
||||||
|
if (posthogConfig.project_api_key && posthogConfig.api_host) {
|
||||||
|
this.posthog.init(posthogConfig.project_api_key, {
|
||||||
|
api_host: posthogConfig.api_host,
|
||||||
|
autocapture: false,
|
||||||
|
mask_all_text: true,
|
||||||
|
mask_all_element_attributes: true,
|
||||||
|
capture_pageview: false,
|
||||||
|
sanitize_properties: this.sanitizeProperties,
|
||||||
|
respect_dnt: true,
|
||||||
|
advanced_disable_decide: true,
|
||||||
|
});
|
||||||
|
this.enabled = true;
|
||||||
|
} else {
|
||||||
|
this.enabled = false;
|
||||||
|
}
|
||||||
|
this.startListeningToSettingsChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeProperties = (
|
||||||
|
properties: Properties,
|
||||||
|
_eventName: string
|
||||||
|
): Properties => {
|
||||||
|
// Callback from posthog to sanitize properties before sending them to the server.
|
||||||
|
// Here we sanitize posthog's built in properties which leak PII e.g. url reporting.
|
||||||
|
// See utils.js _.info.properties in posthog-js.
|
||||||
|
|
||||||
|
if (this.anonymity == Anonymity.Anonymous) {
|
||||||
|
// drop referrer information for anonymous users
|
||||||
|
properties["$referrer"] = null;
|
||||||
|
properties["$referring_domain"] = null;
|
||||||
|
properties["$initial_referrer"] = null;
|
||||||
|
properties["$initial_referring_domain"] = null;
|
||||||
|
|
||||||
|
// drop device ID, which is a UUID persisted in local storage
|
||||||
|
properties["$device_id"] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
private registerSuperProperties(properties: Properties) {
|
||||||
|
if (this.enabled) {
|
||||||
|
this.posthog.register(properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getPlatformProperties(): PlatformProperties {
|
||||||
|
const appVersion = import.meta.env.VITE_APP_VERSION || "unknown";
|
||||||
|
return {
|
||||||
|
appVersion,
|
||||||
|
matrixBackend: widget ? "embedded" : "jssdk",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private capture(
|
||||||
|
eventName: string,
|
||||||
|
properties: Properties,
|
||||||
|
options?: CaptureOptions
|
||||||
|
) {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.posthog.capture(eventName, { ...properties }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEnabled(): boolean {
|
||||||
|
return this.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnonymity(anonymity: Anonymity): void {
|
||||||
|
// Update this.anonymity.
|
||||||
|
// To update the anonymity typically you want to call updateAnonymityFromSettings
|
||||||
|
// to ensure this value is in step with the user's settings.
|
||||||
|
if (
|
||||||
|
this.enabled &&
|
||||||
|
(anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)
|
||||||
|
) {
|
||||||
|
// when transitioning to Disabled or Anonymous ensure we clear out any prior state
|
||||||
|
// set in posthog e.g. distinct ID
|
||||||
|
this.posthog.reset();
|
||||||
|
// Restore any previously set platform super properties
|
||||||
|
this.updateSuperProperties();
|
||||||
|
}
|
||||||
|
this.anonymity = anonymity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getRandomAnalyticsId(): string {
|
||||||
|
return [...crypto.getRandomValues(new Uint8Array(16))]
|
||||||
|
.map((c) => c.toString(16))
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async identifyUser(analyticsIdGenerator: () => string): Promise<void> {
|
||||||
|
// There might be a better way to get the client here.
|
||||||
|
const client = window.matrixclient;
|
||||||
|
|
||||||
|
if (this.anonymity == Anonymity.Pseudonymous) {
|
||||||
|
// 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_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.
|
||||||
|
// Note there's a race condition here - if two devices do these steps at the same time, last write
|
||||||
|
// wins, and the first writer will send tracking with an ID that doesn't match the one on the server
|
||||||
|
// 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(
|
||||||
|
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
||||||
|
Object.assign({ id: analyticsID }, accountData)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.posthog.identify(analyticsID);
|
||||||
|
} catch (e) {
|
||||||
|
// The above could fail due to network requests, but not essential to starting the application,
|
||||||
|
// so swallow it.
|
||||||
|
logger.log("Unable to identify user for tracking" + e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAnonymity(): Anonymity {
|
||||||
|
return this.anonymity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public logout(): void {
|
||||||
|
if (this.enabled) {
|
||||||
|
this.posthog.reset();
|
||||||
|
}
|
||||||
|
this.setAnonymity(Anonymity.Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateSuperProperties(): Promise<void> {
|
||||||
|
// Update super properties in posthog with our platform (app version, platform).
|
||||||
|
// These properties will be subsequently passed in every event.
|
||||||
|
//
|
||||||
|
// This only needs to be done once per page lifetime. Note that getPlatformProperties
|
||||||
|
// is async and can involve a network request if we are running in a browser.
|
||||||
|
this.platformSuperProperties = PosthogAnalytics.getPlatformProperties();
|
||||||
|
this.registerSuperProperties({
|
||||||
|
...this.platformSuperProperties,
|
||||||
|
registrationType:
|
||||||
|
this.registrationType == RegistrationType.Guest
|
||||||
|
? "Guest"
|
||||||
|
: "Registered",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private userRegisteredInThisSession(): boolean {
|
||||||
|
return this.eventSignup.getSignupEndTime() > new Date(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateAnonymityFromSettings(
|
||||||
|
pseudonymousOptIn: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
// Update this.anonymity based on the user's analytics opt-in settings
|
||||||
|
const anonymity = pseudonymousOptIn
|
||||||
|
? Anonymity.Pseudonymous
|
||||||
|
: Anonymity.Disabled;
|
||||||
|
this.setAnonymity(anonymity);
|
||||||
|
if (anonymity === Anonymity.Pseudonymous) {
|
||||||
|
await this.identifyUser(PosthogAnalytics.getRandomAnalyticsId);
|
||||||
|
if (this.userRegisteredInThisSession()) {
|
||||||
|
this.eventSignup.track();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anonymity !== Anonymity.Disabled) {
|
||||||
|
await this.updateSuperProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackEvent<E extends IPosthogEvent>(
|
||||||
|
{ eventName, ...properties }: E,
|
||||||
|
options?: IPostHogEventOptions
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
this.anonymity == Anonymity.Disabled ||
|
||||||
|
this.anonymity == Anonymity.Anonymous
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this.capture(eventName, properties, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
settingsBus.on("opt-in-analytics", (optInAnalytics) => {
|
||||||
|
this.updateAnonymityFromSettings(optInAnalytics);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRegistrationType(registrationType: RegistrationType): void {
|
||||||
|
this.registrationType = registrationType;
|
||||||
|
if (
|
||||||
|
this.anonymity == Anonymity.Disabled ||
|
||||||
|
this.anonymity == Anonymity.Anonymous
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this.updateSuperProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Events
|
||||||
|
|
||||||
|
public eventCallEnded = new CallEndedTracker();
|
||||||
|
public eventSignup = new SignupTracker();
|
||||||
|
public eventCallStarted = new CallStartedTracker();
|
||||||
|
public eventLogin = new LoginTracker();
|
||||||
|
public eventMuteMicrophone = new MuteMicrophoneTracker();
|
||||||
|
public eventMuteCamera = new MuteCameraTracker();
|
||||||
|
}
|
||||||
145
src/PosthogEvents.ts
Normal file
145
src/PosthogEvents.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
IPosthogEvent,
|
||||||
|
PosthogAnalytics,
|
||||||
|
RegistrationType,
|
||||||
|
} from "./PosthogAnalytics";
|
||||||
|
|
||||||
|
interface CallEnded extends IPosthogEvent {
|
||||||
|
eventName: "CallEnded";
|
||||||
|
callName: string;
|
||||||
|
callParticipantsOnLeave: number;
|
||||||
|
callParticipantsMax: number;
|
||||||
|
callDuration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CallEndedTracker {
|
||||||
|
private cache: { startTime: Date; maxParticipantsCount: number } = {
|
||||||
|
startTime: new Date(0),
|
||||||
|
maxParticipantsCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
cacheStartCall(time: Date) {
|
||||||
|
this.cache.startTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheParticipantCountChanged(count: number) {
|
||||||
|
this.cache.maxParticipantsCount = Math.max(
|
||||||
|
count,
|
||||||
|
this.cache.maxParticipantsCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
track(callName: string, callParticipantsNow: number) {
|
||||||
|
PosthogAnalytics.instance.trackEvent<CallEnded>({
|
||||||
|
eventName: "CallEnded",
|
||||||
|
callName,
|
||||||
|
callParticipantsMax: this.cache.maxParticipantsCount,
|
||||||
|
callParticipantsOnLeave: callParticipantsNow,
|
||||||
|
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CallStarted extends IPosthogEvent {
|
||||||
|
eventName: "CallStarted";
|
||||||
|
callName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CallStartedTracker {
|
||||||
|
track(callName: string) {
|
||||||
|
PosthogAnalytics.instance.trackEvent<CallStarted>({
|
||||||
|
eventName: "CallStarted",
|
||||||
|
callName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Signup extends IPosthogEvent {
|
||||||
|
eventName: "Signup";
|
||||||
|
signupDuration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SignupTracker {
|
||||||
|
private cache: { signupStart: Date; signupEnd: Date } = {
|
||||||
|
signupStart: new Date(0),
|
||||||
|
signupEnd: new Date(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
cacheSignupStart(time: Date) {
|
||||||
|
this.cache.signupStart = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSignupEndTime() {
|
||||||
|
return this.cache.signupEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheSignupEnd(time: Date) {
|
||||||
|
this.cache.signupEnd = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
track() {
|
||||||
|
PosthogAnalytics.instance.trackEvent<Signup>({
|
||||||
|
eventName: "Signup",
|
||||||
|
signupDuration:
|
||||||
|
new Date().getSeconds() - this.cache.signupStart.getSeconds(),
|
||||||
|
});
|
||||||
|
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Registered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Login extends IPosthogEvent {
|
||||||
|
eventName: "Login";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginTracker {
|
||||||
|
track() {
|
||||||
|
PosthogAnalytics.instance.trackEvent<Login>({
|
||||||
|
eventName: "Login",
|
||||||
|
});
|
||||||
|
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Registered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MuteMicrophone {
|
||||||
|
eventName: "MuteMicrophone";
|
||||||
|
targetMuteState: "mute" | "unmute";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MuteMicrophoneTracker {
|
||||||
|
track(targetIsMute: boolean) {
|
||||||
|
PosthogAnalytics.instance.trackEvent<MuteMicrophone>({
|
||||||
|
eventName: "MuteMicrophone",
|
||||||
|
targetMuteState: targetIsMute ? "mute" : "unmute",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MuteCamera {
|
||||||
|
eventName: "MuteCamera";
|
||||||
|
targetMuteState: "mute" | "unmute";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MuteCameraTracker {
|
||||||
|
track(targetIsMute: boolean) {
|
||||||
|
PosthogAnalytics.instance.trackEvent<MuteCamera>({
|
||||||
|
eventName: "MuteCamera",
|
||||||
|
targetMuteState: targetIsMute ? "mute" : "unmute",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,7 @@ import { defaultHomeserver, defaultHomeserverHost } from "../matrix-utils";
|
||||||
import styles from "./LoginPage.module.css";
|
import styles from "./LoginPage.module.css";
|
||||||
import { useInteractiveLogin } from "./useInteractiveLogin";
|
import { useInteractiveLogin } from "./useInteractiveLogin";
|
||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||||
|
|
||||||
export const LoginPage: FC = () => {
|
export const LoginPage: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -64,6 +65,7 @@ export const LoginPage: FC = () => {
|
||||||
} else {
|
} else {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
|
PosthogAnalytics.instance.eventLogin.track();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import { LoadingView } from "../FullScreenView";
|
||||||
import { useRecaptcha } from "./useRecaptcha";
|
import { useRecaptcha } from "./useRecaptcha";
|
||||||
import { Caption, Link } from "../typography/Typography";
|
import { Caption, Link } from "../typography/Typography";
|
||||||
import { usePageTitle } from "../usePageTitle";
|
import { usePageTitle } from "../usePageTitle";
|
||||||
|
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||||
|
|
||||||
export const RegisterPage: FC = () => {
|
export const RegisterPage: FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -98,6 +99,7 @@ export const RegisterPage: FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setClient(newClient, session);
|
setClient(newClient, session);
|
||||||
|
PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date());
|
||||||
};
|
};
|
||||||
|
|
||||||
submit()
|
submit()
|
||||||
|
|
@ -142,6 +144,8 @@ export const RegisterPage: FC = () => {
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
|
} else {
|
||||||
|
PosthogAnalytics.instance.eventSignup.cacheSignupStart(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export interface IConfigOptions {
|
export interface IConfigOptions {
|
||||||
posthog?: {
|
posthog?: {
|
||||||
api_key: string;
|
api_key: string;
|
||||||
|
api_host: string;
|
||||||
};
|
};
|
||||||
sentry?: {
|
sentry?: {
|
||||||
DSN: string;
|
DSN: string;
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,7 @@ function useGroupCallState(
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
groupCall: GroupCall,
|
groupCall: GroupCall,
|
||||||
showPollCallStats: boolean
|
showPollCallStats: boolean
|
||||||
) {
|
): InspectorContextState {
|
||||||
const [state, dispatch] = useReducer(reducer, {
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
localUserId: client.getUserId(),
|
localUserId: client.getUserId(),
|
||||||
localSessionId: client.getSessionId(),
|
localSessionId: client.getSessionId(),
|
||||||
|
|
@ -410,11 +410,13 @@ function useGroupCallState(
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupCallInspectorProps {
|
interface GroupCallInspectorProps {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
groupCall: GroupCall;
|
groupCall: GroupCall;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupCallInspector({
|
export function GroupCallInspector({
|
||||||
client,
|
client,
|
||||||
groupCall,
|
groupCall,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import { CallEndedView } from "./CallEndedView";
|
||||||
import { useRoomAvatar } from "./useRoomAvatar";
|
import { useRoomAvatar } from "./useRoomAvatar";
|
||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
|
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
import { findDeviceByName, getDevices } from "../media-utils";
|
import { findDeviceByName, getDevices } from "../media-utils";
|
||||||
|
|
||||||
|
|
@ -170,6 +171,12 @@ export function GroupCallView({
|
||||||
|
|
||||||
const onLeave = useCallback(() => {
|
const onLeave = useCallback(() => {
|
||||||
setLeft(true);
|
setLeft(true);
|
||||||
|
|
||||||
|
PosthogAnalytics.instance.eventCallEnded.track(
|
||||||
|
groupCall.room.name,
|
||||||
|
groupCall.participants.length
|
||||||
|
);
|
||||||
|
|
||||||
leave();
|
leave();
|
||||||
if (widget) {
|
if (widget) {
|
||||||
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
||||||
|
|
@ -179,7 +186,14 @@ export function GroupCallView({
|
||||||
if (!isPasswordlessUser && !isEmbedded) {
|
if (!isPasswordlessUser && !isEmbedded) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
}, [leave, isPasswordlessUser, isEmbedded, history]);
|
}, [
|
||||||
|
groupCall.room.name,
|
||||||
|
groupCall.participants.length,
|
||||||
|
leave,
|
||||||
|
isPasswordlessUser,
|
||||||
|
isEmbedded,
|
||||||
|
history,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget && state === GroupCallState.Entered) {
|
if (widget && state === GroupCallState.Entered) {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import { useAudioContext } from "../video-grid/useMediaStream";
|
||||||
import { useFullscreen } from "../video-grid/useFullscreen";
|
import { useFullscreen } from "../video-grid/useFullscreen";
|
||||||
import { AudioContainer } from "../video-grid/AudioContainer";
|
import { AudioContainer } from "../video-grid/AudioContainer";
|
||||||
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
||||||
|
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||||
import { widget, ElementWidgetActions } from "../widget";
|
import { widget, ElementWidgetActions } from "../widget";
|
||||||
import { useJoinRule } from "./useJoinRule";
|
import { useJoinRule } from "./useJoinRule";
|
||||||
import { useUrlParams } from "../UrlParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
|
|
@ -210,6 +211,9 @@ export function InCallView({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
|
||||||
|
participants.length
|
||||||
|
);
|
||||||
// add the screenshares too
|
// add the screenshares too
|
||||||
for (const screenshareFeed of screenshareFeeds) {
|
for (const screenshareFeed of screenshareFeeds) {
|
||||||
const userMediaItem = tileDescriptors.find(
|
const userMediaItem = tileDescriptors.find(
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { useTranslation } from "react-i18next";
|
||||||
import { IWidgetApiRequest } from "matrix-widget-api";
|
import { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
|
|
||||||
import { usePageUnload } from "./usePageUnload";
|
import { usePageUnload } from "./usePageUnload";
|
||||||
|
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||||
import { TranslatedError, translatedError } from "../TranslatedError";
|
import { TranslatedError, translatedError } from "../TranslatedError";
|
||||||
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
||||||
|
|
||||||
|
|
@ -280,6 +281,9 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||||
|
PosthogAnalytics.instance.eventCallStarted.track(groupCall.room.name);
|
||||||
|
|
||||||
groupCall.enter().catch((error) => {
|
groupCall.enter().catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
updateState({ error });
|
updateState({ error });
|
||||||
|
|
@ -289,11 +293,15 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
const leave = useCallback(() => groupCall.leave(), [groupCall]);
|
const leave = useCallback(() => groupCall.leave(), [groupCall]);
|
||||||
|
|
||||||
const toggleLocalVideoMuted = useCallback(() => {
|
const toggleLocalVideoMuted = useCallback(() => {
|
||||||
groupCall.setLocalVideoMuted(!groupCall.isLocalVideoMuted());
|
const toggleToMute = !groupCall.isLocalVideoMuted();
|
||||||
|
groupCall.setLocalVideoMuted(toggleToMute);
|
||||||
|
PosthogAnalytics.instance.eventMuteCamera.track(toggleToMute);
|
||||||
}, [groupCall]);
|
}, [groupCall]);
|
||||||
|
|
||||||
const toggleMicrophoneMuted = useCallback(() => {
|
const toggleMicrophoneMuted = useCallback(() => {
|
||||||
groupCall.setMicrophoneMuted(!groupCall.isMicrophoneMuted());
|
const toggleToMute = !groupCall.isMicrophoneMuted();
|
||||||
|
groupCall.setMicrophoneMuted(toggleToMute);
|
||||||
|
PosthogAnalytics.instance.eventMuteMicrophone.track(toggleToMute);
|
||||||
}, [groupCall]);
|
}, [groupCall]);
|
||||||
|
|
||||||
const toggleScreensharing = useCallback(async () => {
|
const toggleScreensharing = useCallback(async () => {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ import { ReactComponent as VideoIcon } from "../icons/Video.svg";
|
||||||
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
|
||||||
import { SelectInput } from "../input/SelectInput";
|
import { SelectInput } from "../input/SelectInput";
|
||||||
import { useMediaHandler } from "./useMediaHandler";
|
import { useMediaHandler } from "./useMediaHandler";
|
||||||
import { useSpatialAudio, useShowInspector } from "./useSetting";
|
import {
|
||||||
|
useSpatialAudio,
|
||||||
|
useShowInspector,
|
||||||
|
useOptInAnalytics,
|
||||||
|
} from "./useSetting";
|
||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useDownloadDebugLog } from "./submit-rageshake";
|
import { useDownloadDebugLog } from "./submit-rageshake";
|
||||||
|
|
@ -53,6 +57,7 @@ export const SettingsModal = (props: Props) => {
|
||||||
|
|
||||||
const [spatialAudio, setSpatialAudio] = useSpatialAudio();
|
const [spatialAudio, setSpatialAudio] = useSpatialAudio();
|
||||||
const [showInspector, setShowInspector] = useShowInspector();
|
const [showInspector, setShowInspector] = useShowInspector();
|
||||||
|
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
||||||
|
|
||||||
const downloadDebugLog = useDownloadDebugLog();
|
const downloadDebugLog = useDownloadDebugLog();
|
||||||
|
|
||||||
|
|
@ -115,6 +120,18 @@ export const SettingsModal = (props: Props) => {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="optInAnalytics"
|
||||||
|
label="Allow analytics"
|
||||||
|
type="checkbox"
|
||||||
|
checked={optInAnalytics}
|
||||||
|
description="This will send anonymized data such as the duration of a call the and number of participants to the element call team to help us optimizing the application based on how it is used."
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setOptInAnalytics(event.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem
|
<TabItem
|
||||||
title={
|
title={
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { EventEmitter } from "events";
|
||||||
import { useMemo, useState, useEffect, useCallback } from "react";
|
import { useMemo, useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
// Bus to notify other useSetting consumers when a setting is changed
|
// Bus to notify other useSetting consumers when a setting is changed
|
||||||
const settingsBus = new EventEmitter();
|
export const settingsBus = new EventEmitter();
|
||||||
|
|
||||||
// Like useState, but reads from and persists the value to localStorage
|
// Like useState, but reads from and persists the value to localStorage
|
||||||
const useSetting = <T>(
|
const useSetting = <T>(
|
||||||
|
|
@ -54,3 +54,4 @@ const useSetting = <T>(
|
||||||
|
|
||||||
export const useSpatialAudio = () => useSetting("spatial-audio", false);
|
export const useSpatialAudio = () => useSetting("spatial-audio", false);
|
||||||
export const useShowInspector = () => useSetting("show-inspector", false);
|
export const useShowInspector = () => useSetting("show-inspector", false);
|
||||||
|
export const useOptInAnalytics = () => useSetting("opt-in-analytics", false);
|
||||||
|
|
|
||||||
23
yarn.lock
23
yarn.lock
|
|
@ -2403,6 +2403,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7"
|
||||||
integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==
|
integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==
|
||||||
|
|
||||||
|
"@sentry/types@^7.2.0":
|
||||||
|
version "7.13.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.13.0.tgz#398e33e5c92ea0ce91e2c86e3ab003fe00c471a2"
|
||||||
|
integrity sha512-ttckM1XaeyHRLMdr79wmGA5PFbTGx2jio9DCD/mkEpSfk6OGfqfC7gpwy7BNstDH/VKyQj/lDCJPnwvWqARMoQ==
|
||||||
|
|
||||||
"@sentry/utils@6.19.7":
|
"@sentry/utils@6.19.7":
|
||||||
version "6.19.7"
|
version "6.19.7"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.7.tgz#6edd739f8185fd71afe49cbe351c1bbf5e7b7c79"
|
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.7.tgz#6edd739f8185fd71afe49cbe351c1bbf5e7b7c79"
|
||||||
|
|
@ -7638,6 +7643,11 @@ fetch-retry@^5.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-5.0.3.tgz#edfa3641892995f9afee94f25b168827aa97fe3d"
|
resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-5.0.3.tgz#edfa3641892995f9afee94f25b168827aa97fe3d"
|
||||||
integrity sha512-uJQyMrX5IJZkhoEUBQ3EjxkeiZkppBd5jS/fMTJmfZxLSiaQjv2zD0kTvuvkSH89uFvgSlB6ueGpjD3HWN7Bxw==
|
integrity sha512-uJQyMrX5IJZkhoEUBQ3EjxkeiZkppBd5jS/fMTJmfZxLSiaQjv2zD0kTvuvkSH89uFvgSlB6ueGpjD3HWN7Bxw==
|
||||||
|
|
||||||
|
fflate@^0.4.1:
|
||||||
|
version "0.4.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
|
||||||
|
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
|
||||||
|
|
||||||
figgy-pudding@^3.5.1:
|
figgy-pudding@^3.5.1:
|
||||||
version "3.5.2"
|
version "3.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
|
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
|
||||||
|
|
@ -11680,6 +11690,15 @@ postcss@^8.4.13:
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
|
posthog-js@^1.29.0:
|
||||||
|
version "1.31.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.31.0.tgz#43ffb5a11948a5b10af75e749108936a07519963"
|
||||||
|
integrity sha512-d6vBb/ChS+t33voi37HA76etwWIukEcvJLZLZvkhJZcIrR29shwkAFUzd8lL7VdAelLlaAtmoPMwr820Yq5GUg==
|
||||||
|
dependencies:
|
||||||
|
"@sentry/types" "^7.2.0"
|
||||||
|
fflate "^0.4.1"
|
||||||
|
rrweb-snapshot "^1.1.14"
|
||||||
|
|
||||||
prelude-ls@^1.2.1:
|
prelude-ls@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
|
|
@ -12585,6 +12604,10 @@ rollup@^2.59.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
rrweb-snapshot@^1.1.14:
|
||||||
|
version "1.1.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.14.tgz#9d4d9be54a28a893373428ee4393ec7e5bd83fcc"
|
||||||
|
integrity sha512-eP5pirNjP5+GewQfcOQY4uBiDnpqxNRc65yKPW0eSoU1XamDfc4M8oqpXGMyUyvLyxFDB0q0+DChuxxiU2FXBQ==
|
||||||
rsvp@^4.8.2:
|
rsvp@^4.8.2:
|
||||||
version "4.8.5"
|
version "4.8.5"
|
||||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue