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", | ||||
|     "pako": "^2.0.4", | ||||
|     "postcss-preset-env": "^7", | ||||
|     "posthog-js": "^1.29.0", | ||||
|     "re-resizable": "^6.9.0", | ||||
|     "react": "18", | ||||
|     "react-dom": "18", | ||||
|  |  | |||
|  | @ -38,6 +38,7 @@ import { | |||
|   fallbackICEServerAllowed, | ||||
| } from "./matrix-utils"; | ||||
| import { widget } from "./widget"; | ||||
| import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics"; | ||||
| import { translatedError } from "./TranslatedError"; | ||||
| 
 | ||||
| declare global { | ||||
|  | @ -114,7 +115,9 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|       if (widget) { | ||||
|         // We're inside a widget, so let's engage *matryoshka mode*
 | ||||
|         logger.log("Using a matryoshka client"); | ||||
| 
 | ||||
|         PosthogAnalytics.instance.setRegistrationType( | ||||
|           RegistrationType.Registered | ||||
|         ); | ||||
|         return { | ||||
|           client: await widget.client, | ||||
|           isPasswordlessUser: false, | ||||
|  | @ -132,6 +135,11 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|             session; | ||||
| 
 | ||||
|           try { | ||||
|             PosthogAnalytics.instance.setRegistrationType( | ||||
|               passwordlessUser | ||||
|                 ? RegistrationType.Guest | ||||
|                 : RegistrationType.Registered | ||||
|             ); | ||||
|             return { | ||||
|               client: await initClient( | ||||
|                 { | ||||
|  | @ -279,6 +287,7 @@ export const ClientProvider: FC<Props> = ({ children }) => { | |||
|       error: undefined, | ||||
|     }); | ||||
|     history.push("/"); | ||||
|     PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest); | ||||
|   }, [history, client]); | ||||
| 
 | ||||
|   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 { useInteractiveLogin } from "./useInteractiveLogin"; | ||||
| import { usePageTitle } from "../usePageTitle"; | ||||
| import { PosthogAnalytics } from "../PosthogAnalytics"; | ||||
| 
 | ||||
| export const LoginPage: FC = () => { | ||||
|   const { t } = useTranslation(); | ||||
|  | @ -64,6 +65,7 @@ export const LoginPage: FC = () => { | |||
|           } else { | ||||
|             history.push("/"); | ||||
|           } | ||||
|           PosthogAnalytics.instance.eventLogin.track(); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           setError(error); | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ import { LoadingView } from "../FullScreenView"; | |||
| import { useRecaptcha } from "./useRecaptcha"; | ||||
| import { Caption, Link } from "../typography/Typography"; | ||||
| import { usePageTitle } from "../usePageTitle"; | ||||
| import { PosthogAnalytics } from "../PosthogAnalytics"; | ||||
| 
 | ||||
| export const RegisterPage: FC = () => { | ||||
|   const { t } = useTranslation(); | ||||
|  | @ -98,6 +99,7 @@ export const RegisterPage: FC = () => { | |||
|         } | ||||
| 
 | ||||
|         setClient(newClient, session); | ||||
|         PosthogAnalytics.instance.eventSignup.cacheSignupEnd(new Date()); | ||||
|       }; | ||||
| 
 | ||||
|       submit() | ||||
|  | @ -142,6 +144,8 @@ export const RegisterPage: FC = () => { | |||
| 
 | ||||
|   if (loading) { | ||||
|     return <LoadingView />; | ||||
|   } else { | ||||
|     PosthogAnalytics.instance.eventSignup.cacheSignupStart(new Date()); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| export interface IConfigOptions { | ||||
|   posthog?: { | ||||
|     api_key: string; | ||||
|     api_host: string; | ||||
|   }; | ||||
|   sentry?: { | ||||
|     DSN: string; | ||||
|  |  | |||
|  | @ -353,7 +353,7 @@ function useGroupCallState( | |||
|   client: MatrixClient, | ||||
|   groupCall: GroupCall, | ||||
|   showPollCallStats: boolean | ||||
| ) { | ||||
| ): InspectorContextState { | ||||
|   const [state, dispatch] = useReducer(reducer, { | ||||
|     localUserId: client.getUserId(), | ||||
|     localSessionId: client.getSessionId(), | ||||
|  | @ -410,11 +410,13 @@ function useGroupCallState( | |||
| 
 | ||||
|   return state; | ||||
| } | ||||
| 
 | ||||
| interface GroupCallInspectorProps { | ||||
|   client: MatrixClient; | ||||
|   groupCall: GroupCall; | ||||
|   show: boolean; | ||||
| } | ||||
| 
 | ||||
| export function GroupCallInspector({ | ||||
|   client, | ||||
|   groupCall, | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ import { CallEndedView } from "./CallEndedView"; | |||
| import { useRoomAvatar } from "./useRoomAvatar"; | ||||
| import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; | ||||
| import { useLocationNavigation } from "../useLocationNavigation"; | ||||
| import { PosthogAnalytics } from "../PosthogAnalytics"; | ||||
| import { useMediaHandler } from "../settings/useMediaHandler"; | ||||
| import { findDeviceByName, getDevices } from "../media-utils"; | ||||
| 
 | ||||
|  | @ -170,6 +171,12 @@ export function GroupCallView({ | |||
| 
 | ||||
|   const onLeave = useCallback(() => { | ||||
|     setLeft(true); | ||||
| 
 | ||||
|     PosthogAnalytics.instance.eventCallEnded.track( | ||||
|       groupCall.room.name, | ||||
|       groupCall.participants.length | ||||
|     ); | ||||
| 
 | ||||
|     leave(); | ||||
|     if (widget) { | ||||
|       widget.api.transport.send(ElementWidgetActions.HangupCall, {}); | ||||
|  | @ -179,7 +186,14 @@ export function GroupCallView({ | |||
|     if (!isPasswordlessUser && !isEmbedded) { | ||||
|       history.push("/"); | ||||
|     } | ||||
|   }, [leave, isPasswordlessUser, isEmbedded, history]); | ||||
|   }, [ | ||||
|     groupCall.room.name, | ||||
|     groupCall.participants.length, | ||||
|     leave, | ||||
|     isPasswordlessUser, | ||||
|     isEmbedded, | ||||
|     history, | ||||
|   ]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (widget && state === GroupCallState.Entered) { | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ import { useAudioContext } from "../video-grid/useMediaStream"; | |||
| import { useFullscreen } from "../video-grid/useFullscreen"; | ||||
| import { AudioContainer } from "../video-grid/AudioContainer"; | ||||
| import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice"; | ||||
| import { PosthogAnalytics } from "../PosthogAnalytics"; | ||||
| import { widget, ElementWidgetActions } from "../widget"; | ||||
| import { useJoinRule } from "./useJoinRule"; | ||||
| import { useUrlParams } from "../UrlParams"; | ||||
|  | @ -210,6 +211,9 @@ export function InCallView({ | |||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged( | ||||
|       participants.length | ||||
|     ); | ||||
|     // add the screenshares too
 | ||||
|     for (const screenshareFeed of screenshareFeeds) { | ||||
|       const userMediaItem = tileDescriptors.find( | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ import { useTranslation } from "react-i18next"; | |||
| import { IWidgetApiRequest } from "matrix-widget-api"; | ||||
| 
 | ||||
| import { usePageUnload } from "./usePageUnload"; | ||||
| import { PosthogAnalytics } from "../PosthogAnalytics"; | ||||
| import { TranslatedError, translatedError } from "../TranslatedError"; | ||||
| import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget"; | ||||
| 
 | ||||
|  | @ -280,6 +281,9 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { | |||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); | ||||
|     PosthogAnalytics.instance.eventCallStarted.track(groupCall.room.name); | ||||
| 
 | ||||
|     groupCall.enter().catch((error) => { | ||||
|       console.error(error); | ||||
|       updateState({ error }); | ||||
|  | @ -289,11 +293,15 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { | |||
|   const leave = useCallback(() => groupCall.leave(), [groupCall]); | ||||
| 
 | ||||
|   const toggleLocalVideoMuted = useCallback(() => { | ||||
|     groupCall.setLocalVideoMuted(!groupCall.isLocalVideoMuted()); | ||||
|     const toggleToMute = !groupCall.isLocalVideoMuted(); | ||||
|     groupCall.setLocalVideoMuted(toggleToMute); | ||||
|     PosthogAnalytics.instance.eventMuteCamera.track(toggleToMute); | ||||
|   }, [groupCall]); | ||||
| 
 | ||||
|   const toggleMicrophoneMuted = useCallback(() => { | ||||
|     groupCall.setMicrophoneMuted(!groupCall.isMicrophoneMuted()); | ||||
|     const toggleToMute = !groupCall.isMicrophoneMuted(); | ||||
|     groupCall.setMicrophoneMuted(toggleToMute); | ||||
|     PosthogAnalytics.instance.eventMuteMicrophone.track(toggleToMute); | ||||
|   }, [groupCall]); | ||||
| 
 | ||||
|   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 { SelectInput } from "../input/SelectInput"; | ||||
| import { useMediaHandler } from "./useMediaHandler"; | ||||
| import { useSpatialAudio, useShowInspector } from "./useSetting"; | ||||
| import { | ||||
|   useSpatialAudio, | ||||
|   useShowInspector, | ||||
|   useOptInAnalytics, | ||||
| } from "./useSetting"; | ||||
| import { FieldRow, InputField } from "../input/Input"; | ||||
| import { Button } from "../button"; | ||||
| import { useDownloadDebugLog } from "./submit-rageshake"; | ||||
|  | @ -53,6 +57,7 @@ export const SettingsModal = (props: Props) => { | |||
| 
 | ||||
|   const [spatialAudio, setSpatialAudio] = useSpatialAudio(); | ||||
|   const [showInspector, setShowInspector] = useShowInspector(); | ||||
|   const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); | ||||
| 
 | ||||
|   const downloadDebugLog = useDownloadDebugLog(); | ||||
| 
 | ||||
|  | @ -115,6 +120,18 @@ export const SettingsModal = (props: Props) => { | |||
|               } | ||||
|             /> | ||||
|           </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 | ||||
|           title={ | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ import { EventEmitter } from "events"; | |||
| import { useMemo, useState, useEffect, useCallback } from "react"; | ||||
| 
 | ||||
| // 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
 | ||||
| const useSetting = <T>( | ||||
|  | @ -54,3 +54,4 @@ const useSetting = <T>( | |||
| 
 | ||||
| export const useSpatialAudio = () => useSetting("spatial-audio", 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" | ||||
|   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": | ||||
|   version "6.19.7" | ||||
|   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" | ||||
|   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: | ||||
|   version "3.5.2" | ||||
|   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" | ||||
|     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: | ||||
|   version "1.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" | ||||
|  | @ -12585,6 +12604,10 @@ rollup@^2.59.0: | |||
|   optionalDependencies: | ||||
|     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: | ||||
|   version "4.8.5" | ||||
|   resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue