Merge pull request #6495 from matrix-org/posthog-analytics
Add support for Posthog Analytics under a labs flag
This commit is contained in:
commit
a65b41be63
12 changed files with 1403 additions and 2 deletions
|
@ -87,6 +87,7 @@
|
|||
"pako": "^2.0.3",
|
||||
"parse5": "^6.0.1",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.12.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"qrcode": "^1.4.4",
|
||||
"re-resizable": "^6.9.0",
|
||||
|
|
748
src/@types/posthog.d.ts
vendored
Normal file
748
src/@types/posthog.d.ts
vendored
Normal file
|
@ -0,0 +1,748 @@
|
|||
// A clone of the type definitions from posthog-js, stripped of references to transitive
|
||||
// dependencies which we don't actually use, so that we don't need to install them.
|
||||
//
|
||||
// Original file lives in node_modules/posthog/dist/module.d.ts
|
||||
|
||||
/* eslint-disable @typescript-eslint/member-delimiter-style */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
// Type definitions for exported methods
|
||||
|
||||
declare class posthog {
|
||||
/**
|
||||
* This function initializes a new instance of the PostHog capturing object.
|
||||
* All new instances are added to the main posthog object as sub properties (such as
|
||||
* posthog.library_name) and also returned by this function. To define a
|
||||
* second instance on the page, you would call:
|
||||
*
|
||||
* posthog.init('new token', { your: 'config' }, 'library_name');
|
||||
*
|
||||
* and use it like so:
|
||||
*
|
||||
* posthog.library_name.capture(...);
|
||||
*
|
||||
* @param {String} token Your PostHog API token
|
||||
* @param {Object} [config] A dictionary of config options to override. <a href="https://github.com/posthog/posthog-js/blob/6e0e873/src/posthog-core.js#L57-L91">See a list of default config options</a>.
|
||||
* @param {String} [name] The name for the new posthog instance that you want created
|
||||
*/
|
||||
static init(token: string, config?: posthog.Config, name?: string): posthog
|
||||
|
||||
/**
|
||||
* Clears super properties and generates a new random distinct_id for this instance.
|
||||
* Useful for clearing data when a user logs out.
|
||||
*/
|
||||
static reset(reset_device_id?: boolean): void
|
||||
|
||||
/**
|
||||
* Capture an event. This is the most important and
|
||||
* frequently used PostHog function.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* // capture an event named 'Registered'
|
||||
* posthog.capture('Registered', {'Gender': 'Male', 'Age': 21});
|
||||
*
|
||||
* // capture an event using navigator.sendBeacon
|
||||
* posthog.capture('Left page', {'duration_seconds': 35}, {transport: 'sendBeacon'});
|
||||
*
|
||||
* @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc.
|
||||
* @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself.
|
||||
* @param {Object} [options] Optional configuration for this capture request.
|
||||
* @param {String} [options.transport] Transport method for network request ('XHR' or 'sendBeacon').
|
||||
*/
|
||||
static capture(
|
||||
event_name: string,
|
||||
properties?: posthog.Properties,
|
||||
options?: { transport: 'XHR' | 'sendBeacon' }
|
||||
): posthog.CaptureResult
|
||||
|
||||
/**
|
||||
* Capture a page view event, which is currently ignored by the server.
|
||||
* This function is called by default on page load unless the
|
||||
* capture_pageview configuration variable is false.
|
||||
*
|
||||
* @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url.
|
||||
* @api private
|
||||
*/
|
||||
static capture_pageview(page?: string): void
|
||||
|
||||
/**
|
||||
* Register a set of super properties, which are included with all
|
||||
* events. This will overwrite previous super property values.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* // register 'Gender' as a super property
|
||||
* posthog.register({'Gender': 'Female'});
|
||||
*
|
||||
* // register several super properties when a user signs up
|
||||
* posthog.register({
|
||||
* 'Email': 'jdoe@example.com',
|
||||
* 'Account Type': 'Free'
|
||||
* });
|
||||
*
|
||||
* @param {Object} properties An associative array of properties to store about the user
|
||||
* @param {Number} [days] How many days since the user's last visit to store the super properties
|
||||
*/
|
||||
static register(properties: posthog.Properties, days?: number): void
|
||||
|
||||
/**
|
||||
* Register a set of super properties only once. This will not
|
||||
* overwrite previous super property values, unlike register().
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* // register a super property for the first time only
|
||||
* posthog.register_once({
|
||||
* 'First Login Date': new Date().toISOString()
|
||||
* });
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* If default_value is specified, current super properties
|
||||
* with that value will be overwritten.
|
||||
*
|
||||
* @param {Object} properties An associative array of properties to store about the user
|
||||
* @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
|
||||
* @param {Number} [days] How many days since the users last visit to store the super properties
|
||||
*/
|
||||
static register_once(properties: posthog.Properties, default_value?: posthog.Property, days?: number): void
|
||||
|
||||
/**
|
||||
* Delete a super property stored with the current user.
|
||||
*
|
||||
* @param {String} property The name of the super property to remove
|
||||
*/
|
||||
static unregister(property: string): void
|
||||
|
||||
/**
|
||||
* Identify a user with a unique ID instead of a PostHog
|
||||
* randomly generated distinct_id. If the method is never called,
|
||||
* then unique visitors will be identified by a UUID generated
|
||||
* the first time they visit the site.
|
||||
*
|
||||
* If user properties are passed, they are also sent to posthog.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.identify('[user unique id]')
|
||||
* posthog.identify('[user unique id]', { email: 'john@example.com' })
|
||||
* posthog.identify('[user unique id]', {}, { referral_code: '12345' })
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* You can call this function to overwrite a previously set
|
||||
* unique ID for the current user. PostHog cannot translate
|
||||
* between IDs at this time, so when you change a user's ID
|
||||
* they will appear to be a new user.
|
||||
*
|
||||
* When used alone, posthog.identify will change the user's
|
||||
* distinct_id to the unique ID provided. When used in tandem
|
||||
* with posthog.alias, it will allow you to identify based on
|
||||
* unique ID and map that back to the original, anonymous
|
||||
* distinct_id given to the user upon her first arrival to your
|
||||
* site (thus connecting anonymous pre-signup activity to
|
||||
* post-signup activity). Though the two work together, do not
|
||||
* call identify() at the same time as alias(). Calling the two
|
||||
* at the same time can cause a race condition, so it is best
|
||||
* practice to call identify on the original, anonymous ID
|
||||
* right after you've aliased it.
|
||||
*
|
||||
* @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used.
|
||||
* @param {Object} [userProperties] Optional: An associative array of properties to store about the user
|
||||
* @param {Object} [userPropertiesToSetOnce] Optional: An associative array of properties to store about the user. If property is previously set, this does not override that value.
|
||||
*/
|
||||
static identify(
|
||||
unique_id?: string,
|
||||
userPropertiesToSet?: posthog.Properties,
|
||||
userPropertiesToSetOnce?: posthog.Properties
|
||||
): void
|
||||
|
||||
/**
|
||||
* Create an alias, which PostHog will use to link two distinct_ids going forward (not retroactively).
|
||||
* Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the
|
||||
* following is a valid scenario:
|
||||
*
|
||||
* posthog.alias('new_id', 'existing_id');
|
||||
* ...
|
||||
* posthog.alias('newer_id', 'new_id');
|
||||
*
|
||||
* If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID.
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* The best practice is to call alias() when a unique ID is first created for a user
|
||||
* (e.g., when a user first registers for an account and provides an email address).
|
||||
* alias() should never be called more than once for a given user, except to
|
||||
* chain a newer ID to a previously new ID, as described above.
|
||||
*
|
||||
* @param {String} alias A unique identifier that you want to use for this user in the future.
|
||||
* @param {String} [original] The current identifier being used for this user.
|
||||
*/
|
||||
static alias(alias: string, original?: string): posthog.CaptureResult | number
|
||||
|
||||
/**
|
||||
* Update the configuration of a posthog library instance.
|
||||
*
|
||||
* The default config is:
|
||||
*
|
||||
* {
|
||||
* // HTTP method for capturing requests
|
||||
* api_method: 'POST'
|
||||
*
|
||||
* // transport for sending requests ('XHR' or 'sendBeacon')
|
||||
* // NB: sendBeacon should only be used for scenarios such as
|
||||
* // page unload where a "best-effort" attempt to send is
|
||||
* // acceptable; the sendBeacon API does not support callbacks
|
||||
* // or any way to know the result of the request. PostHog
|
||||
* // capturing via sendBeacon will not support any event-
|
||||
* // batching or retry mechanisms.
|
||||
* api_transport: 'XHR'
|
||||
*
|
||||
* // Automatically capture clicks, form submissions and change events
|
||||
* autocapture: true
|
||||
*
|
||||
* // Capture rage clicks (beta) - useful for session recording
|
||||
* rageclick: false
|
||||
*
|
||||
* // super properties cookie expiration (in days)
|
||||
* cookie_expiration: 365
|
||||
*
|
||||
* // super properties span subdomains
|
||||
* cross_subdomain_cookie: true
|
||||
*
|
||||
* // debug mode
|
||||
* debug: false
|
||||
*
|
||||
* // if this is true, the posthog cookie or localStorage entry
|
||||
* // will be deleted, and no user persistence will take place
|
||||
* disable_persistence: false
|
||||
*
|
||||
* // if this is true, PostHog will automatically determine
|
||||
* // City, Region and Country data using the IP address of
|
||||
* //the client
|
||||
* ip: true
|
||||
*
|
||||
* // opt users out of capturing by this PostHog instance by default
|
||||
* opt_out_capturing_by_default: false
|
||||
*
|
||||
* // opt users out of browser data storage by this PostHog instance by default
|
||||
* opt_out_persistence_by_default: false
|
||||
*
|
||||
* // persistence mechanism used by opt-in/opt-out methods - cookie
|
||||
* // or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* opt_out_capturing_persistence_type: 'localStorage'
|
||||
*
|
||||
* // customize the name of cookie/localStorage set by opt-in/opt-out methods
|
||||
* opt_out_capturing_cookie_prefix: null
|
||||
*
|
||||
* // type of persistent store for super properties (cookie/
|
||||
* // localStorage) if set to 'localStorage', any existing
|
||||
* // posthog cookie value with the same persistence_name
|
||||
* // will be transferred to localStorage and deleted
|
||||
* persistence: 'cookie'
|
||||
*
|
||||
* // name for super properties persistent store
|
||||
* persistence_name: ''
|
||||
*
|
||||
* // names of properties/superproperties which should never
|
||||
* // be sent with capture() calls
|
||||
* property_blacklist: []
|
||||
*
|
||||
* // if this is true, posthog cookies will be marked as
|
||||
* // secure, meaning they will only be transmitted over https
|
||||
* secure_cookie: false
|
||||
*
|
||||
* // should we capture a page view on page load
|
||||
* capture_pageview: true
|
||||
*
|
||||
* // if you set upgrade to be true, the library will check for
|
||||
* // a cookie from our old js library and import super
|
||||
* // properties from it, then the old cookie is deleted
|
||||
* // The upgrade config option only works in the initialization,
|
||||
* // so make sure you set it when you create the library.
|
||||
* upgrade: false
|
||||
*
|
||||
* // extra HTTP request headers to set for each API request, in
|
||||
* // the format {'Header-Name': value}
|
||||
* xhr_headers: {}
|
||||
*
|
||||
* // protocol for fetching in-app message resources, e.g.
|
||||
* // 'https://' or 'http://'; defaults to '//' (which defers to the
|
||||
* // current page's protocol)
|
||||
* inapp_protocol: '//'
|
||||
*
|
||||
* // whether to open in-app message link in new tab/window
|
||||
* inapp_link_new_window: false
|
||||
*
|
||||
* // a set of rrweb config options that PostHog users can configure
|
||||
* // see https://github.com/rrweb-io/rrweb/blob/master/guide.md
|
||||
* session_recording: {
|
||||
* blockClass: 'ph-no-capture',
|
||||
* blockSelector: null,
|
||||
* ignoreClass: 'ph-ignore-input',
|
||||
* maskAllInputs: false,
|
||||
* maskInputOptions: {},
|
||||
* maskInputFn: null,
|
||||
* slimDOMOptions: {},
|
||||
* collectFonts: false
|
||||
* }
|
||||
*
|
||||
* // prevent autocapture from capturing any attribute names on elements
|
||||
* mask_all_element_attributes: false
|
||||
*
|
||||
* // prevent autocapture from capturing textContent on all elements
|
||||
* mask_all_text: false
|
||||
*
|
||||
* // will disable requests to the /decide endpoint (please review documentation for details)
|
||||
* // autocapture, feature flags, compression and session recording will be disabled when set to `true`
|
||||
* advanced_disable_decide: false
|
||||
*
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @param {Object} config A dictionary of new configuration values to update
|
||||
*/
|
||||
static set_config(config: posthog.Config): void
|
||||
|
||||
/**
|
||||
* returns the current config object for the library.
|
||||
*/
|
||||
static get_config<T extends keyof posthog.Config>(prop_name: T): posthog.Config[T]
|
||||
|
||||
/**
|
||||
* Returns the value of the super property named property_name. If no such
|
||||
* property is set, get_property() will return the undefined value.
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* get_property() can only be called after the PostHog library has finished loading.
|
||||
* init() has a loaded function available to handle this automatically. For example:
|
||||
*
|
||||
* // grab value for 'user_id' after the posthog library has loaded
|
||||
* posthog.init('YOUR PROJECT TOKEN', {
|
||||
* loaded: function(posthog) {
|
||||
* user_id = posthog.get_property('user_id');
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @param {String} property_name The name of the super property you want to retrieve
|
||||
*/
|
||||
static get_property(property_name: string): posthog.Property | undefined
|
||||
|
||||
/**
|
||||
* Returns the current distinct id of the user. This is either the id automatically
|
||||
* generated by the library or the id that has been passed by a call to identify().
|
||||
*
|
||||
* ### Notes:
|
||||
*
|
||||
* get_distinct_id() can only be called after the PostHog library has finished loading.
|
||||
* init() has a loaded function available to handle this automatically. For example:
|
||||
*
|
||||
* // set distinct_id after the posthog library has loaded
|
||||
* posthog.init('YOUR PROJECT TOKEN', {
|
||||
* loaded: function(posthog) {
|
||||
* distinct_id = posthog.get_distinct_id();
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
static get_distinct_id(): string
|
||||
|
||||
/**
|
||||
* Opt the user out of data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* // opt user out
|
||||
* posthog.opt_out_capturing();
|
||||
*
|
||||
* // opt user out with different cookie configuration from PostHog instance
|
||||
* posthog.opt_out_capturing({
|
||||
* cookie_expiration: 30,
|
||||
* secure_cookie: true
|
||||
* });
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {boolean} [options.clear_persistence=true] If true, will delete all data stored by the sdk in persistence
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
||||
*/
|
||||
static opt_out_capturing(options?: posthog.OptInOutCapturingOptions): void
|
||||
|
||||
/**
|
||||
* Opt the user in to data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* // opt user in
|
||||
* posthog.opt_in_capturing();
|
||||
*
|
||||
* // opt user in with specific event name, properties, cookie configuration
|
||||
* posthog.opt_in_capturing({
|
||||
* capture_event_name: 'User opted in',
|
||||
* capture_event_properties: {
|
||||
* 'Email': 'jdoe@example.com'
|
||||
* },
|
||||
* cookie_expiration: 30,
|
||||
* secure_cookie: true
|
||||
* });
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {function} [options.capture] Function used for capturing a PostHog event to record the opt-in action (default is this PostHog instance's capture method)
|
||||
* @param {string} [options.capture_event_name=$opt_in] Event name to be used for capturing the opt-in action
|
||||
* @param {Object} [options.capture_properties] Set of properties to be captured along with the opt-in action
|
||||
* @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
||||
*/
|
||||
static opt_in_capturing(options?: posthog.OptInOutCapturingOptions): void
|
||||
|
||||
/**
|
||||
* Check whether the user has opted out of data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* const has_opted_out = posthog.has_opted_out_capturing();
|
||||
* // use has_opted_out value
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @returns {boolean} current opt-out status
|
||||
*/
|
||||
static has_opted_out_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean
|
||||
|
||||
/**
|
||||
* Check whether the user has opted in to data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* const has_opted_in = posthog.has_opted_in_capturing();
|
||||
* // use has_opted_in value
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @returns {boolean} current opt-in status
|
||||
*/
|
||||
static has_opted_in_capturing(options?: posthog.HasOptedInOutCapturingOptions): boolean
|
||||
|
||||
/**
|
||||
* Clear the user's opt in/out status of data capturing and cookies/localstorage for this PostHog instance
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* // clear user's opt-in/out status
|
||||
* posthog.clear_opt_in_out_capturing();
|
||||
*
|
||||
* // clear user's opt-in/out status with specific cookie configuration - should match
|
||||
* // configuration used when opt_in_capturing/opt_out_capturing methods were called.
|
||||
* posthog.clear_opt_in_out_capturing({
|
||||
* cookie_expiration: 30,
|
||||
* secure_cookie: true
|
||||
* });
|
||||
*
|
||||
* @param {Object} [options] A dictionary of config options to override
|
||||
* @param {boolean} [options.enable_persistence=true] If true, will re-enable sdk persistence
|
||||
* @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable
|
||||
* @param {string} [options.cookie_prefix=__ph_opt_in_out] Custom prefix to be used in the cookie/localstorage name
|
||||
* @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this PostHog instance's config)
|
||||
* @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this PostHog instance's config)
|
||||
*/
|
||||
static clear_opt_in_out_capturing(options?: posthog.ClearOptInOutCapturingOptions): void
|
||||
|
||||
/*
|
||||
* See if feature flag is enabled for user.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* if(posthog.isFeatureEnabled('beta-feature')) { // do something }
|
||||
*
|
||||
* @param {Object|String} prop Key of the feature flag.
|
||||
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
|
||||
*/
|
||||
static isFeatureEnabled(key: string, options?: posthog.isFeatureEnabledOptions): boolean
|
||||
|
||||
/*
|
||||
* See if feature flags are available.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.onFeatureFlags(function(featureFlags) { // do something })
|
||||
*
|
||||
* @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user.
|
||||
*/
|
||||
static onFeatureFlags(callback: (flags: string[]) => void): false | undefined
|
||||
|
||||
/*
|
||||
* Reload all feature flags for the user.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.reloadFeatureFlags()
|
||||
*/
|
||||
static reloadFeatureFlags(): void
|
||||
|
||||
static toString(): string
|
||||
|
||||
/* Will log all capture requests to the Javascript console, including event properties for easy debugging */
|
||||
static debug(): void
|
||||
|
||||
/*
|
||||
* Starts session recording and updates disable_session_recording to false.
|
||||
* Used for manual session recording management. By default, session recording is enabled and
|
||||
* starts automatically.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.startSessionRecording()
|
||||
*/
|
||||
static startSessionRecording(): void
|
||||
|
||||
/*
|
||||
* Stops session recording and updates disable_session_recording to true.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.stopSessionRecording()
|
||||
*/
|
||||
static stopSessionRecording(): void
|
||||
|
||||
/*
|
||||
* Check if session recording is currently running.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* const isSessionRecordingOn = posthog.sessionRecordingStarted()
|
||||
*/
|
||||
static sessionRecordingStarted(): boolean
|
||||
}
|
||||
|
||||
declare namespace posthog {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type Property = any;
|
||||
type Properties = Record<string, Property>;
|
||||
type CaptureResult = { event: string; properties: Properties } | undefined;
|
||||
type CaptureCallback = (response: any, data: any) => void;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface Config {
|
||||
api_host?: string
|
||||
api_method?: string
|
||||
api_transport?: string
|
||||
autocapture?: boolean
|
||||
rageclick?: boolean
|
||||
cdn?: string
|
||||
cross_subdomain_cookie?: boolean
|
||||
persistence?: 'localStorage' | 'cookie' | 'memory'
|
||||
persistence_name?: string
|
||||
cookie_name?: string
|
||||
loaded?: (posthog_instance: typeof posthog) => void
|
||||
store_google?: boolean
|
||||
save_referrer?: boolean
|
||||
test?: boolean
|
||||
verbose?: boolean
|
||||
img?: boolean
|
||||
capture_pageview?: boolean
|
||||
debug?: boolean
|
||||
cookie_expiration?: number
|
||||
upgrade?: boolean
|
||||
disable_session_recording?: boolean
|
||||
disable_persistence?: boolean
|
||||
disable_cookie?: boolean
|
||||
secure_cookie?: boolean
|
||||
ip?: boolean
|
||||
opt_out_capturing_by_default?: boolean
|
||||
opt_out_persistence_by_default?: boolean
|
||||
opt_out_capturing_persistence_type?: 'localStorage' | 'cookie'
|
||||
opt_out_capturing_cookie_prefix?: string | null
|
||||
respect_dnt?: boolean
|
||||
property_blacklist?: string[]
|
||||
xhr_headers?: { [header_name: string]: string }
|
||||
inapp_protocol?: string
|
||||
inapp_link_new_window?: boolean
|
||||
request_batching?: boolean
|
||||
sanitize_properties?: (properties: posthog.Properties, event_name: string) => posthog.Properties
|
||||
properties_string_max_length?: number
|
||||
mask_all_element_attributes?: boolean
|
||||
mask_all_text?: boolean
|
||||
advanced_disable_decide?: boolean
|
||||
}
|
||||
|
||||
interface OptInOutCapturingOptions {
|
||||
clear_persistence: boolean
|
||||
persistence_type: string
|
||||
cookie_prefix: string
|
||||
cookie_expiration: number
|
||||
cross_subdomain_cookie: boolean
|
||||
secure_cookie: boolean
|
||||
}
|
||||
|
||||
interface HasOptedInOutCapturingOptions {
|
||||
persistence_type: string
|
||||
cookie_prefix: string
|
||||
}
|
||||
|
||||
interface ClearOptInOutCapturingOptions {
|
||||
enable_persistence: boolean
|
||||
persistence_type: string
|
||||
cookie_prefix: string
|
||||
cookie_expiration: number
|
||||
cross_subdomain_cookie: boolean
|
||||
secure_cookie: boolean
|
||||
}
|
||||
|
||||
interface isFeatureEnabledOptions {
|
||||
send_event: boolean
|
||||
}
|
||||
|
||||
export class persistence {
|
||||
static properties(): posthog.Properties
|
||||
|
||||
static load(): void
|
||||
|
||||
static save(): void
|
||||
|
||||
static remove(): void
|
||||
|
||||
static clear(): void
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {*=} default_value
|
||||
* @param {number=} days
|
||||
*/
|
||||
static register_once(props: Properties, default_value?: Property, days?: number): boolean
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {number=} days
|
||||
*/
|
||||
static register(props: posthog.Properties, days?: number): boolean
|
||||
|
||||
static unregister(prop: string): void
|
||||
|
||||
static update_campaign_params(): void
|
||||
|
||||
static update_search_keyword(referrer: string): void
|
||||
|
||||
static update_referrer_info(referrer: string): void
|
||||
|
||||
static get_referrer_info(): posthog.Properties
|
||||
|
||||
static safe_merge(props: posthog.Properties): posthog.Properties
|
||||
|
||||
static update_config(config: posthog.Config): void
|
||||
|
||||
static set_disabled(disabled: boolean): void
|
||||
|
||||
static set_cross_subdomain(cross_subdomain: boolean): void
|
||||
|
||||
static get_cross_subdomain(): boolean
|
||||
|
||||
static set_secure(secure: boolean): void
|
||||
|
||||
static set_event_timer(event_name: string, timestamp: Date): void
|
||||
|
||||
static remove_event_timer(event_name: string): Date | undefined
|
||||
}
|
||||
|
||||
export class people {
|
||||
/*
|
||||
* Set properties on a user record.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.people.set('gender', 'm');
|
||||
*
|
||||
* // or set multiple properties at once
|
||||
* posthog.people.set({
|
||||
* 'Company': 'Acme',
|
||||
* 'Plan': 'Premium',
|
||||
* 'Upgrade date': new Date()
|
||||
* });
|
||||
* // properties can be strings, integers, dates, or lists
|
||||
*
|
||||
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
|
||||
* @param {*} [to] A value to set on the given property name
|
||||
* @param {Function} [callback] If provided, the callback will be called after capturing the event.
|
||||
*/
|
||||
static set(
|
||||
prop: posthog.Properties | string,
|
||||
to?: posthog.Property,
|
||||
callback?: posthog.CaptureCallback
|
||||
): posthog.Properties
|
||||
|
||||
/*
|
||||
* Set properties on a user record, only if they do not yet exist.
|
||||
* This will not overwrite previous people property values, unlike
|
||||
* people.set().
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.people.set_once('First Login Date', new Date());
|
||||
*
|
||||
* // or set multiple properties at once
|
||||
* posthog.people.set_once({
|
||||
* 'First Login Date': new Date(),
|
||||
* 'Starting Plan': 'Premium'
|
||||
* });
|
||||
*
|
||||
* // properties can be strings, integers or dates
|
||||
*
|
||||
* @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values.
|
||||
* @param {*} [to] A value to set on the given property name
|
||||
* @param {Function} [callback] If provided, the callback will be called after capturing the event.
|
||||
*/
|
||||
static set_once(
|
||||
prop: posthog.Properties | string,
|
||||
to?: posthog.Property,
|
||||
callback?: posthog.CaptureCallback
|
||||
): posthog.Properties
|
||||
|
||||
static toString(): string
|
||||
}
|
||||
|
||||
export class featureFlags {
|
||||
static getFlags(): string[]
|
||||
|
||||
static reloadFeatureFlags(): void
|
||||
|
||||
/*
|
||||
* See if feature flag is enabled for user.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* if(posthog.isFeatureEnabled('beta-feature')) { // do something }
|
||||
*
|
||||
* @param {Object|String} prop Key of the feature flag.
|
||||
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
|
||||
*/
|
||||
static isFeatureEnabled(key: string, options?: { send_event?: boolean }): boolean
|
||||
|
||||
/*
|
||||
* See if feature flags are available.
|
||||
*
|
||||
* ### Usage:
|
||||
*
|
||||
* posthog.onFeatureFlags(function(featureFlags) { // do something })
|
||||
*
|
||||
* @param {Function} [callback] The callback function will be called once the feature flags are ready. It'll return a list of feature flags enabled for the user.
|
||||
*/
|
||||
static onFeatureFlags(callback: (flags: string[]) => void): false | undefined
|
||||
}
|
||||
|
||||
export class feature_flags extends featureFlags {}
|
||||
}
|
||||
|
||||
export type PostHog = typeof posthog;
|
||||
|
||||
export default posthog;
|
|
@ -48,6 +48,7 @@ import { Jitsi } from "./widgets/Jitsi";
|
|||
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform";
|
||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
import CallHandler from './CallHandler';
|
||||
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
|
@ -573,6 +574,8 @@ async function doSetLoggedIn(
|
|||
await abortLogin();
|
||||
}
|
||||
|
||||
PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
|
||||
|
||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
||||
|
||||
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||
|
@ -700,6 +703,8 @@ export function logout(): void {
|
|||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||
}
|
||||
|
||||
PosthogAnalytics.instance.logout();
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
// logout doesn't work for guest sessions
|
||||
// Also we sometimes want to re-log in a guest session if we abort the login.
|
||||
|
|
355
src/PosthogAnalytics.ts
Normal file
355
src/PosthogAnalytics.ts
Normal file
|
@ -0,0 +1,355 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import posthog, { PostHog } from 'posthog-js';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
|
||||
/* 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 `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e.
|
||||
* hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256.
|
||||
* - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e.
|
||||
* redact all matrix identifiers in tracking events.
|
||||
* - If both flags are false or not set, events are not sent.
|
||||
*/
|
||||
|
||||
interface IEvent {
|
||||
// The event name that will be used by PostHog. Event names should use snake_case.
|
||||
eventName: string;
|
||||
|
||||
// The properties of the event that will be stored in PostHog. This is just a placeholder,
|
||||
// extending interfaces must override this with a concrete definition to do type validation.
|
||||
properties: {};
|
||||
}
|
||||
|
||||
export enum Anonymity {
|
||||
Disabled,
|
||||
Anonymous,
|
||||
Pseudonymous
|
||||
}
|
||||
|
||||
// If an event extends IPseudonymousEvent, the event contains pseudonymous data
|
||||
// that won't be sent unless the user has explicitly consented to pseudonymous tracking.
|
||||
// For example, it might contain hashed user IDs or room IDs.
|
||||
// Such events will be automatically dropped if PosthogAnalytics.anonymity isn't set to Pseudonymous.
|
||||
export interface IPseudonymousEvent extends IEvent {}
|
||||
|
||||
// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data;
|
||||
// i.e. no identifiers that can be associated with the user.
|
||||
export interface IAnonymousEvent extends IEvent {}
|
||||
|
||||
export interface IRoomEvent extends IPseudonymousEvent {
|
||||
hashedRoomId: string;
|
||||
}
|
||||
|
||||
interface IPageView extends IAnonymousEvent {
|
||||
eventName: "$pageview";
|
||||
properties: {
|
||||
durationMs?: number;
|
||||
screen?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const hashHex = async (input: string): Promise<string> => {
|
||||
const buf = new TextEncoder().encode(input);
|
||||
const digestBuf = await window.crypto.subtle.digest("sha-256", buf);
|
||||
return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join("");
|
||||
};
|
||||
|
||||
const whitelistedScreens = new Set([
|
||||
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
|
||||
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
|
||||
]);
|
||||
|
||||
export async function getRedactedCurrentLocation(
|
||||
origin: string,
|
||||
hash: string,
|
||||
pathname: string,
|
||||
anonymity: Anonymity,
|
||||
): Promise<string> {
|
||||
// Redact PII from the current location.
|
||||
// If anonymous is true, redact entirely, if false, substitute it with a hash.
|
||||
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
|
||||
if (origin.startsWith('file://')) {
|
||||
pathname = "/<redacted_file_scheme_url>/";
|
||||
}
|
||||
|
||||
let hashStr;
|
||||
if (hash == "") {
|
||||
hashStr = "";
|
||||
} else {
|
||||
let [beforeFirstSlash, screen, ...parts] = hash.split("/");
|
||||
|
||||
if (!whitelistedScreens.has(screen)) {
|
||||
screen = "<redacted_screen_name>";
|
||||
}
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
parts[i] = anonymity === Anonymity.Anonymous ? `<redacted>` : await hashHex(parts[i]);
|
||||
}
|
||||
|
||||
hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`;
|
||||
}
|
||||
return origin + pathname + hashStr;
|
||||
}
|
||||
|
||||
interface PlatformProperties {
|
||||
appVersion: string;
|
||||
appPlatform: 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 all identifers will be redacted before being passed to posthog
|
||||
* - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being 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 IAnonymousEvent, IPseudonymousEvent or IRoomEvent.
|
||||
* 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is
|
||||
* Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled.
|
||||
*/
|
||||
|
||||
private anonymity = Anonymity.Disabled;
|
||||
// set true during the constructor if posthog config is present, otherwise false
|
||||
private enabled = false;
|
||||
private static _instance = null;
|
||||
private platformSuperProperties = {};
|
||||
|
||||
public static get instance(): PosthogAnalytics {
|
||||
if (!this._instance) {
|
||||
this._instance = new PosthogAnalytics(posthog);
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
constructor(private readonly posthog: PostHog) {
|
||||
const posthogConfig = SdkConfig.get()["posthog"];
|
||||
if (posthogConfig) {
|
||||
this.posthog.init(posthogConfig.projectApiKey, {
|
||||
api_host: posthogConfig.apiHost,
|
||||
autocapture: false,
|
||||
mask_all_text: true,
|
||||
mask_all_element_attributes: true,
|
||||
// This only triggers on page load, which for our SPA isn't particularly useful.
|
||||
// Plus, the .capture call originating from somewhere in posthog makes it hard
|
||||
// to redact URLs, which requires async code.
|
||||
//
|
||||
// To raise this manually, just call .capture("$pageview") or posthog.capture_pageview.
|
||||
capture_pageview: false,
|
||||
sanitize_properties: this.sanitizeProperties,
|
||||
respect_dnt: true,
|
||||
});
|
||||
this.enabled = true;
|
||||
} else {
|
||||
this.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeProperties = (properties: posthog.Properties): posthog.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.
|
||||
|
||||
// Replace the $current_url with a redacted version.
|
||||
// $redacted_current_url is injected by this class earlier in capture(), as its generation
|
||||
// is async and can't be done in this non-async callback.
|
||||
if (!properties['$redacted_current_url']) {
|
||||
console.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely");
|
||||
}
|
||||
properties['$current_url'] = properties['$redacted_current_url'];
|
||||
delete properties['$redacted_current_url'];
|
||||
|
||||
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 static getAnonymityFromSettings(): Anonymity {
|
||||
// determine the current anonymity level based on current user settings
|
||||
|
||||
// "Send anonymous usage data which helps us improve Element. This will use a cookie."
|
||||
const analyticsOptIn = SettingsStore.getValue("analyticsOptIn", null, true);
|
||||
|
||||
// (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie."
|
||||
//
|
||||
// TODO: Currently, this is only a labs flag, for testing purposes.
|
||||
const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymous_analytics_opt_in", null, true);
|
||||
|
||||
let anonymity;
|
||||
if (pseudonumousOptIn) {
|
||||
anonymity = Anonymity.Pseudonymous;
|
||||
} else if (analyticsOptIn) {
|
||||
anonymity = Anonymity.Anonymous;
|
||||
} else {
|
||||
anonymity = Anonymity.Disabled;
|
||||
}
|
||||
|
||||
return anonymity;
|
||||
}
|
||||
|
||||
private registerSuperProperties(properties: posthog.Properties) {
|
||||
if (this.enabled) {
|
||||
this.posthog.register(properties);
|
||||
}
|
||||
}
|
||||
|
||||
private static async getPlatformProperties(): Promise<PlatformProperties> {
|
||||
const platform = PlatformPeg.get();
|
||||
let appVersion;
|
||||
try {
|
||||
appVersion = await platform.getAppVersion();
|
||||
} catch (e) {
|
||||
// this happens if no version is set i.e. in dev
|
||||
appVersion = "unknown";
|
||||
}
|
||||
|
||||
return {
|
||||
appVersion,
|
||||
appPlatform: platform.getHumanReadableName(),
|
||||
};
|
||||
}
|
||||
|
||||
private async capture(eventName: string, properties: posthog.Properties) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
const { origin, hash, pathname } = window.location;
|
||||
properties['$redacted_current_url'] = await getRedactedCurrentLocation(
|
||||
origin, hash, pathname, this.anonymity);
|
||||
this.posthog.capture(eventName, properties);
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public setAnonymity(anonymity: Anonymity): void {
|
||||
// Update this.anonymity.
|
||||
// This is public for testing purposes, 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.registerSuperProperties(this.platformSuperProperties);
|
||||
}
|
||||
this.anonymity = anonymity;
|
||||
}
|
||||
|
||||
public async identifyUser(userId: string): Promise<void> {
|
||||
if (this.anonymity == Anonymity.Pseudonymous) {
|
||||
this.posthog.identify(await hashHex(userId));
|
||||
}
|
||||
}
|
||||
|
||||
public getAnonymity(): Anonymity {
|
||||
return this.anonymity;
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
if (this.enabled) {
|
||||
this.posthog.reset();
|
||||
}
|
||||
this.setAnonymity(Anonymity.Anonymous);
|
||||
}
|
||||
|
||||
public async trackPseudonymousEvent<E extends IPseudonymousEvent>(
|
||||
eventName: E["eventName"],
|
||||
properties: E["properties"] = {},
|
||||
) {
|
||||
if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return;
|
||||
await this.capture(eventName, properties);
|
||||
}
|
||||
|
||||
public async trackAnonymousEvent<E extends IAnonymousEvent>(
|
||||
eventName: E["eventName"],
|
||||
properties: E["properties"] = {},
|
||||
): Promise<void> {
|
||||
if (this.anonymity == Anonymity.Disabled) return;
|
||||
await this.capture(eventName, properties);
|
||||
}
|
||||
|
||||
public async trackRoomEvent<E extends IRoomEvent>(
|
||||
eventName: E["eventName"],
|
||||
roomId: string,
|
||||
properties: Omit<E["properties"], "roomId">,
|
||||
): Promise<void> {
|
||||
const updatedProperties = {
|
||||
...properties,
|
||||
hashedRoomId: roomId ? await hashHex(roomId) : null,
|
||||
};
|
||||
await this.trackPseudonymousEvent(eventName, updatedProperties);
|
||||
}
|
||||
|
||||
public async trackPageView(durationMs: number): Promise<void> {
|
||||
const hash = window.location.hash;
|
||||
|
||||
let screen = null;
|
||||
const split = hash.split("/");
|
||||
if (split.length >= 2) {
|
||||
screen = split[1];
|
||||
}
|
||||
|
||||
await this.trackAnonymousEvent<IPageView>("$pageview", {
|
||||
durationMs,
|
||||
screen,
|
||||
});
|
||||
}
|
||||
|
||||
public async updatePlatformSuperProperties(): 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 = await PosthogAnalytics.getPlatformProperties();
|
||||
this.registerSuperProperties(this.platformSuperProperties);
|
||||
}
|
||||
|
||||
public async updateAnonymityFromSettings(userId?: string): Promise<void> {
|
||||
// Update this.anonymity based on the user's analytics opt-in settings
|
||||
// Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
|
||||
this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
|
||||
if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
|
||||
await this.identifyUser(userId);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -107,6 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
|||
import SoftLogout from './auth/SoftLogout';
|
||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import { copyPlaintext } from "../../utils/strings";
|
||||
import { PosthogAnalytics } from '../../PosthogAnalytics';
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -387,6 +388,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
if (SettingsStore.getValue("analyticsOptIn")) {
|
||||
Analytics.enable();
|
||||
}
|
||||
|
||||
PosthogAnalytics.instance.updateAnonymityFromSettings();
|
||||
PosthogAnalytics.instance.updatePlatformSuperProperties();
|
||||
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||
}
|
||||
|
||||
|
@ -443,6 +448,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const durationMs = this.stopPageChangeTimer();
|
||||
Analytics.trackPageChange(durationMs);
|
||||
CountlyAnalytics.instance.trackPageChange(durationMs);
|
||||
PosthogAnalytics.instance.trackPageView(durationMs);
|
||||
}
|
||||
if (this.focusComposer) {
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
|
|
|
@ -36,6 +36,7 @@ import { UIFeature } from "../../../../../settings/UIFeature";
|
|||
import { isE2eAdvancedPanelPossible } from "../../E2eAdvancedPanel";
|
||||
import CountlyAnalytics from "../../../../../CountlyAnalytics";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";
|
||||
|
||||
export class IgnoredUser extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -106,6 +107,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
_updateAnalytics = (checked) => {
|
||||
checked ? Analytics.enable() : Analytics.disable();
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ !checked);
|
||||
PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId());
|
||||
};
|
||||
|
||||
_onExportE2eKeysClicked = () => {
|
||||
|
|
|
@ -813,6 +813,7 @@
|
|||
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
|
||||
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
|
||||
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
|
||||
"Send pseudonymous analytics data": "Send pseudonymous analytics data",
|
||||
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
|
||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||
"New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
|
||||
|
|
|
@ -41,6 +41,7 @@ import { Layout } from "./Layout";
|
|||
import ReducedMotionController from './controllers/ReducedMotionController';
|
||||
import IncompatibleController from "./controllers/IncompatibleController";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController';
|
||||
import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
|
||||
|
||||
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
|
||||
|
@ -268,6 +269,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
},
|
||||
"feature_pseudonymous_analytics_opt_in": {
|
||||
isFeature: true,
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td('Send pseudonymous analytics data'),
|
||||
default: false,
|
||||
controller: new PseudonymousAnalyticsController(),
|
||||
},
|
||||
"advancedRoomListLogging": {
|
||||
// TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231
|
||||
displayName: _td("Enable advanced debugging for the room list"),
|
||||
|
|
26
src/settings/controllers/PseudonymousAnalyticsController.ts
Normal file
26
src/settings/controllers/PseudonymousAnalyticsController.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingController from "./SettingController";
|
||||
import { SettingLevel } from "../SettingLevel";
|
||||
import { PosthogAnalytics } from "../../PosthogAnalytics";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
|
||||
export default class PseudonymousAnalyticsController extends SettingController {
|
||||
public onChange(level: SettingLevel, roomId: string, newValue: any) {
|
||||
PosthogAnalytics.instance.updateAnonymityFromSettings(MatrixClientPeg.get().getUserId());
|
||||
}
|
||||
}
|
232
test/PosthogAnalytics-test.ts
Normal file
232
test/PosthogAnalytics-test.ts
Normal file
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Anonymity,
|
||||
getRedactedCurrentLocation,
|
||||
IAnonymousEvent,
|
||||
IPseudonymousEvent,
|
||||
IRoomEvent,
|
||||
PosthogAnalytics,
|
||||
} from '../src/PosthogAnalytics';
|
||||
|
||||
import SdkConfig from '../src/SdkConfig';
|
||||
|
||||
class FakePosthog {
|
||||
public capture;
|
||||
public init;
|
||||
public identify;
|
||||
public reset;
|
||||
public register;
|
||||
|
||||
constructor() {
|
||||
this.capture = jest.fn();
|
||||
this.init = jest.fn();
|
||||
this.identify = jest.fn();
|
||||
this.reset = jest.fn();
|
||||
this.register = jest.fn();
|
||||
}
|
||||
}
|
||||
|
||||
export interface ITestEvent extends IAnonymousEvent {
|
||||
key: "jest_test_event";
|
||||
properties: {
|
||||
foo: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITestPseudonymousEvent extends IPseudonymousEvent {
|
||||
key: "jest_test_pseudo_event";
|
||||
properties: {
|
||||
foo: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITestRoomEvent extends IRoomEvent {
|
||||
key: "jest_test_room_event";
|
||||
properties: {
|
||||
foo: string;
|
||||
};
|
||||
}
|
||||
|
||||
describe("PosthogAnalytics", () => {
|
||||
let fakePosthog: FakePosthog;
|
||||
const shaHashes = {
|
||||
"42": "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049",
|
||||
"some": "a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b",
|
||||
"pii": "bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4",
|
||||
"foo": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fakePosthog = new FakePosthog();
|
||||
|
||||
window.crypto = {
|
||||
subtle: {
|
||||
digest: async (_, encodedMessage) => {
|
||||
const message = new TextDecoder().decode(encodedMessage);
|
||||
const hexHash = shaHashes[message];
|
||||
const bytes = [];
|
||||
for (let c = 0; c < hexHash.length; c += 2) {
|
||||
bytes.push(parseInt(hexHash.substr(c, 2), 16));
|
||||
}
|
||||
return bytes;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.crypto = null;
|
||||
});
|
||||
|
||||
describe("Initialisation", () => {
|
||||
it("Should not be enabled without config being set", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockReturnValue({});
|
||||
const analytics = new PosthogAnalytics(fakePosthog);
|
||||
expect(analytics.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("Should be enabled if config is set", () => {
|
||||
jest.spyOn(SdkConfig, "get").mockReturnValue({
|
||||
posthog: {
|
||||
projectApiKey: "foo",
|
||||
apiHost: "bar",
|
||||
},
|
||||
});
|
||||
const analytics = new PosthogAnalytics(fakePosthog);
|
||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||
expect(analytics.isEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tracking", () => {
|
||||
let analytics: PosthogAnalytics;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SdkConfig, "get").mockReturnValue({
|
||||
posthog: {
|
||||
projectApiKey: "foo",
|
||||
apiHost: "bar",
|
||||
},
|
||||
});
|
||||
|
||||
analytics = new PosthogAnalytics(fakePosthog);
|
||||
});
|
||||
|
||||
it("Should pass trackAnonymousEvent() to posthog", async () => {
|
||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
|
||||
foo: "bar",
|
||||
});
|
||||
expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event");
|
||||
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
||||
});
|
||||
|
||||
it("Should pass trackRoomEvent to posthog", async () => {
|
||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||
const roomId = "42";
|
||||
await analytics.trackRoomEvent<IRoomEvent>("jest_test_event", roomId, {
|
||||
foo: "bar",
|
||||
});
|
||||
expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event");
|
||||
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
||||
expect(fakePosthog.capture.mock.calls[0][1]["hashedRoomId"])
|
||||
.toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049");
|
||||
});
|
||||
|
||||
it("Should pass trackPseudonymousEvent() to posthog", async () => {
|
||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_pseudo_event", {
|
||||
foo: "bar",
|
||||
});
|
||||
expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_pseudo_event");
|
||||
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
||||
});
|
||||
|
||||
it("Should not track pseudonymous messages if anonymous", async () => {
|
||||
analytics.setAnonymity(Anonymity.Anonymous);
|
||||
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_event", {
|
||||
foo: "bar",
|
||||
});
|
||||
expect(fakePosthog.capture.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it("Should not track any events if disabled", async () => {
|
||||
analytics.setAnonymity(Anonymity.Disabled);
|
||||
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_event", {
|
||||
foo: "bar",
|
||||
});
|
||||
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
|
||||
foo: "bar",
|
||||
});
|
||||
await analytics.trackRoomEvent<ITestRoomEvent>("room id", "foo", {
|
||||
foo: "bar",
|
||||
});
|
||||
await analytics.trackPageView(200);
|
||||
expect(fakePosthog.capture.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it("Should pseudonymise a location of a known screen", async () => {
|
||||
const location = await getRedactedCurrentLocation(
|
||||
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous);
|
||||
expect(location).toBe(
|
||||
`https://foo.bar/#/register/\
|
||||
a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
|
||||
bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
|
||||
});
|
||||
|
||||
it("Should anonymise a location of a known screen", async () => {
|
||||
const location = await getRedactedCurrentLocation(
|
||||
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous);
|
||||
expect(location).toBe("https://foo.bar/#/register/<redacted>/<redacted>");
|
||||
});
|
||||
|
||||
it("Should pseudonymise a location of an unknown screen", async () => {
|
||||
const location = await getRedactedCurrentLocation(
|
||||
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous);
|
||||
expect(location).toBe(
|
||||
`https://foo.bar/#/<redacted_screen_name>/\
|
||||
a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
|
||||
bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
|
||||
});
|
||||
|
||||
it("Should anonymise a location of an unknown screen", async () => {
|
||||
const location = await getRedactedCurrentLocation(
|
||||
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous);
|
||||
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>/<redacted>");
|
||||
});
|
||||
|
||||
it("Should handle an empty hash", async () => {
|
||||
const location = await getRedactedCurrentLocation(
|
||||
"https://foo.bar", "", "/", Anonymity.Anonymous);
|
||||
expect(location).toBe("https://foo.bar/");
|
||||
});
|
||||
|
||||
it("Should identify the user to posthog if pseudonymous", async () => {
|
||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||
await analytics.identifyUser("foo");
|
||||
expect(fakePosthog.identify.mock.calls[0][0])
|
||||
.toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae");
|
||||
});
|
||||
|
||||
it("Should not identify the user to posthog if anonymous", async () => {
|
||||
analytics.setAnonymity(Anonymity.Anonymous);
|
||||
await analytics.identifyUser("foo");
|
||||
expect(fakePosthog.identify.mock.calls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -22,10 +22,15 @@
|
|||
"es2019",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"paths": {
|
||||
"posthog-js": [
|
||||
"./src/@types/posthog.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
]
|
||||
],
|
||||
}
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -3601,6 +3601,11 @@ fbjs@^0.8.4:
|
|||
setimmediate "^1.0.5"
|
||||
ua-parser-js "^0.7.18"
|
||||
|
||||
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==
|
||||
|
||||
file-entry-cache@^6.0.0, file-entry-cache@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
||||
|
@ -6249,6 +6254,13 @@ postcss@^8.0.2:
|
|||
nanoid "^3.1.23"
|
||||
source-map-js "^0.6.2"
|
||||
|
||||
posthog-js@1.12.1:
|
||||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.12.1.tgz#97834ee2574f34ffb5db2f5b07452c847e3c4d27"
|
||||
integrity sha512-Y3lzcWkS8xFY6Ryj3I4ees7qWP2WGkLw0Arcbk5xaT0+5YlA6UC2jlL/+fN9bz/Bl62EoN3BML901Cuot/QNjg==
|
||||
dependencies:
|
||||
fflate "^0.4.1"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
|
|
Loading…
Reference in a new issue