Merge pull request #6495 from matrix-org/posthog-analytics

Add support for Posthog Analytics under a labs flag
pull/21833/head
James Salter 2021-08-02 14:46:17 +01:00 committed by GitHub
commit a65b41be63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1403 additions and 2 deletions

View File

@ -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
View 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;

View File

@ -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
View 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);
}
}
}

View File

@ -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);

View File

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

View File

@ -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)",

View File

@ -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"),

View 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());
}
}

View 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);
});
});
});

View File

@ -22,10 +22,15 @@
"es2019",
"dom",
"dom.iterable"
]
],
"paths": {
"posthog-js": [
"./src/@types/posthog.d.ts"
]
}
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
]
],
}

View File

@ -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"