This commit is contained in:
983
react/features/analytics/AnalyticsEvents.ts
Normal file
983
react/features/analytics/AnalyticsEvents.ts
Normal file
@@ -0,0 +1,983 @@
|
||||
/**
|
||||
* The constant for the event type 'track'.
|
||||
* TODO: keep these constants in a single place. Can we import them from
|
||||
* lib-jitsi-meet's AnalyticsEvents somehow?
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const TYPE_TRACK = 'track';
|
||||
|
||||
/**
|
||||
* The constant for the event type 'UI' (User Interaction).
|
||||
* TODO: keep these constants in a single place. Can we import them from
|
||||
* lib-jitsi-meet's AnalyticsEvents somehow?
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const TYPE_UI = 'ui';
|
||||
|
||||
/**
|
||||
* The identifier for the "pinned" action. The local participant has pinned a
|
||||
* participant to remain on large video.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
export const ACTION_PINNED = 'pinned';
|
||||
|
||||
/**
|
||||
* The identifier for the "unpinned" action. The local participant has unpinned
|
||||
* a participant so the participant doesn't remain permanently on local large
|
||||
* video.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
export const ACTION_UNPINNED = 'unpinned';
|
||||
|
||||
/**
|
||||
* The identifier for the "pressed" action for shortcut events. This action
|
||||
* means that a button was pressed (and not yet released).
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
export const ACTION_SHORTCUT_PRESSED = 'pressed';
|
||||
|
||||
/**
|
||||
* The identifier for the "released" action for shortcut events. This action
|
||||
* means that a button which was previously pressed was released.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
export const ACTION_SHORTCUT_RELEASED = 'released';
|
||||
|
||||
/**
|
||||
* The identifier for the "triggered" action for shortcut events. This action
|
||||
* means that a button was pressed, and we don't care about whether it was
|
||||
* released or will be released in the future.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
export const ACTION_SHORTCUT_TRIGGERED = 'triggered';
|
||||
|
||||
/**
|
||||
* The name of the keyboard shortcut or toolbar button for muting audio.
|
||||
*/
|
||||
export const AUDIO_MUTE = 'audio.mute';
|
||||
|
||||
/**
|
||||
* The name of the keyboard shortcut or toolbar button for muting desktop sharing.
|
||||
*/
|
||||
export const DESKTOP_MUTE = 'desktop.mute';
|
||||
|
||||
/**
|
||||
* The name of the keyboard shortcut or toolbar button for muting video.
|
||||
*/
|
||||
export const VIDEO_MUTE = 'video.mute';
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that a certain action was requested through
|
||||
* the jitsi-meet API.
|
||||
*
|
||||
* @param {string} action - The action which was requested through the
|
||||
* jitsi-meet API.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createApiEvent(action: string, attributes = {}) {
|
||||
return {
|
||||
action,
|
||||
attributes,
|
||||
source: 'jitsi-meet-api'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that the audio-only mode has been changed.
|
||||
*
|
||||
* @param {boolean} enabled - True if audio-only is enabled, false otherwise.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createAudioOnlyChangedEvent(enabled: boolean) {
|
||||
return {
|
||||
action: `audio.only.${enabled ? 'enabled' : 'disabled'}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for about the JitsiConnection.
|
||||
*
|
||||
* @param {string} action - The action that the event represents.
|
||||
* @param {boolean} attributes - Additional attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createConnectionEvent(action: string, attributes = {}) {
|
||||
return {
|
||||
action,
|
||||
actionSubject: 'connection',
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates an action occurred in the calendar
|
||||
* integration UI.
|
||||
*
|
||||
* @param {string} eventName - The name of the calendar UI event.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createCalendarClickedEvent(eventName: string, attributes = {}) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: eventName,
|
||||
attributes,
|
||||
source: 'calendar',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that the calendar container is shown and
|
||||
* selected.
|
||||
*
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createCalendarSelectedEvent(attributes = {}) {
|
||||
return {
|
||||
action: 'selected',
|
||||
attributes,
|
||||
source: 'calendar',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event indicating that a calendar has been connected.
|
||||
*
|
||||
* @param {boolean} attributes - Additional attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createCalendarConnectedEvent(attributes = {}) {
|
||||
return {
|
||||
action: 'connected',
|
||||
actionSubject: 'calendar',
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates an action occurred in the recent list
|
||||
* integration UI.
|
||||
*
|
||||
* @param {string} eventName - The name of the recent list UI event.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRecentClickedEvent(eventName: string, attributes = {}) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: eventName,
|
||||
attributes,
|
||||
source: 'recent.list',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicate an action occurred in the chrome extension banner.
|
||||
*
|
||||
* @param {boolean} installPressed - Whether the user pressed install or `x` - cancel.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createChromeExtensionBannerEvent(installPressed: boolean, attributes = {}) {
|
||||
return {
|
||||
action: installPressed ? 'install' : 'cancel',
|
||||
attributes,
|
||||
source: 'chrome.extension.banner',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that the recent list container is shown and
|
||||
* selected.
|
||||
*
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRecentSelectedEvent(attributes = {}) {
|
||||
return {
|
||||
action: 'selected',
|
||||
attributes,
|
||||
source: 'recent.list',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for an action on the deep linking page.
|
||||
*
|
||||
* @param {string} action - The action that the event represents.
|
||||
* @param {string} actionSubject - The subject that was acted upon.
|
||||
* @param {boolean} attributes - Additional attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createDeepLinkingPageEvent(
|
||||
action: string, actionSubject: string, attributes = {}) {
|
||||
return {
|
||||
action,
|
||||
actionSubject,
|
||||
source: 'deepLinkingPage',
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that a device was changed.
|
||||
*
|
||||
* @param {string} mediaType - The media type of the device ('audio' or
|
||||
* 'video').
|
||||
* @param {string} deviceType - The type of the device ('input' or 'output').
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createDeviceChangedEvent(mediaType: string, deviceType: string) {
|
||||
return {
|
||||
action: 'device.changed',
|
||||
attributes: {
|
||||
'device_type': deviceType,
|
||||
'media_type': mediaType
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event indicating that an action related to E2EE occurred.
|
||||
*
|
||||
* @param {string} action - The action which occurred.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createE2EEEvent(action: string) {
|
||||
return {
|
||||
action,
|
||||
actionSubject: 'e2ee'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which specifies that the feedback dialog has been opened.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createFeedbackOpenEvent() {
|
||||
return {
|
||||
action: 'feedback.opened'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for an action regarding the AddPeopleDialog (invites).
|
||||
*
|
||||
* @param {string} action - The action that the event represents.
|
||||
* @param {string} actionSubject - The subject that was acted upon.
|
||||
* @param {boolean} attributes - Additional attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createInviteDialogEvent(
|
||||
action: string, actionSubject: string, attributes = {}) {
|
||||
return {
|
||||
action,
|
||||
actionSubject,
|
||||
attributes,
|
||||
source: 'inviteDialog'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which reports about the current network information reported by the operating system.
|
||||
*
|
||||
* @param {boolean} isOnline - Tells whether or not the internet is reachable.
|
||||
* @param {string} [networkType] - Network type, see {@code NetworkInfo} type defined by the 'base/net-info' feature.
|
||||
* @param {Object} [details] - Extra info, see {@code NetworkInfo} type defined by the 'base/net-info' feature.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function createNetworkInfoEvent({ isOnline, networkType, details }:
|
||||
{ details?: Object; isOnline: boolean; networkType?: string; }) {
|
||||
const attributes: {
|
||||
details?: Object;
|
||||
isOnline: boolean;
|
||||
networkType?: string;
|
||||
} = { isOnline };
|
||||
|
||||
// Do no include optional stuff or Amplitude handler will log warnings.
|
||||
networkType && (attributes.networkType = networkType);
|
||||
details && (attributes.details = details);
|
||||
|
||||
return {
|
||||
action: 'network.info',
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "not allowed error" event.
|
||||
*
|
||||
* @param {string} type - The type of the error.
|
||||
* @param {string} reason - The reason for the error.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createNotAllowedErrorEvent(type: string, reason: string) {
|
||||
return {
|
||||
action: 'not.allowed.error',
|
||||
attributes: {
|
||||
reason,
|
||||
type
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an "offer/answer failure" event.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createOfferAnswerFailedEvent() {
|
||||
return {
|
||||
action: 'offer.answer.failure'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "page reload" event.
|
||||
*
|
||||
* @param {string} reason - The reason for the reload.
|
||||
* @param {number} timeout - The timeout in seconds after which the page is
|
||||
* scheduled to reload.
|
||||
* @param {Object} details - The details for the error.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createPageReloadScheduledEvent(reason: string, timeout: number, details: Object = {}) {
|
||||
return {
|
||||
action: 'page.reload.scheduled',
|
||||
attributes: {
|
||||
reason,
|
||||
timeout,
|
||||
...details
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "pinned" or "unpinned" event.
|
||||
*
|
||||
* @param {string} action - The action ("pinned" or "unpinned").
|
||||
* @param {string} participantId - The ID of the participant which was pinned.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createPinnedEvent(action: string, participantId: string, attributes = {}) {
|
||||
return {
|
||||
type: TYPE_TRACK,
|
||||
action,
|
||||
actionSubject: 'participant',
|
||||
objectType: 'participant',
|
||||
objectId: participantId,
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a poll event.
|
||||
* The following events will be created:
|
||||
* - poll.created
|
||||
* - poll.vote.checked
|
||||
* - poll.vote.sent
|
||||
* - poll.vote.skipped
|
||||
* - poll.vote.detailsViewed
|
||||
* - poll.vote.changed
|
||||
* - poll.option.added
|
||||
* - poll.option.moved
|
||||
* - poll.option.removed.
|
||||
*
|
||||
* @param {string} action - The action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function createPollEvent(action: string) {
|
||||
return {
|
||||
action: `poll.${action}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that a button in the profile panel was
|
||||
* clicked.
|
||||
*
|
||||
* @param {string} buttonName - The name of the button.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createProfilePanelButtonEvent(buttonName: string, attributes = {}) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: buttonName,
|
||||
attributes,
|
||||
source: 'profile.panel',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that a specific button on one of the
|
||||
* recording-related dialogs was clicked.
|
||||
*
|
||||
* @param {string} dialogName - The name of the dialog (e.g. 'start' or 'stop').
|
||||
* @param {string} buttonName - The name of the button (e.g. 'confirm' or
|
||||
* 'cancel').
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRecordingDialogEvent(
|
||||
dialogName: string, buttonName: string, attributes = {}) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: buttonName,
|
||||
attributes,
|
||||
source: `${dialogName}.recording.dialog`,
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that a specific button on one of the
|
||||
* liveStreaming-related dialogs was clicked.
|
||||
*
|
||||
* @param {string} dialogName - The name of the dialog (e.g. 'start' or 'stop').
|
||||
* @param {string} buttonName - The name of the button (e.g. 'confirm' or
|
||||
* 'cancel').
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createLiveStreamingDialogEvent(dialogName: string, buttonName: string) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: buttonName,
|
||||
source: `${dialogName}.liveStreaming.dialog`,
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event with the local tracks duration.
|
||||
*
|
||||
* @param {Object} duration - The object with the duration of the local tracks.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createLocalTracksDurationEvent(duration: {
|
||||
audio: { value: number; };
|
||||
conference: { value: number; };
|
||||
video: {
|
||||
camera: { value: number; };
|
||||
desktop: { value: number; };
|
||||
};
|
||||
}) {
|
||||
const { audio, video, conference } = duration;
|
||||
const { camera, desktop } = video;
|
||||
|
||||
return {
|
||||
action: 'local.tracks.durations',
|
||||
attributes: {
|
||||
audio: audio.value,
|
||||
camera: camera.value,
|
||||
conference: conference.value,
|
||||
desktop: desktop.value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that an action related to recording has
|
||||
* occurred.
|
||||
*
|
||||
* @param {string} action - The action (e.g. 'start' or 'stop').
|
||||
* @param {string} type - The recording type (e.g. 'file' or 'live').
|
||||
* @param {number} value - The duration of the recording in seconds (for stop
|
||||
* action).
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRecordingEvent(action: string, type: string, value?: number) {
|
||||
return {
|
||||
action,
|
||||
actionSubject: `recording.${type}`,
|
||||
attributes: {
|
||||
value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that the same conference has been rejoined.
|
||||
*
|
||||
* @param {string} url - The full conference URL.
|
||||
* @param {number} lastConferenceDuration - How many seconds user stayed in the previous conference.
|
||||
* @param {number} timeSinceLeft - How many seconds since the last conference was left.
|
||||
* @returns {Object} The event in a format suitable for sending via sendAnalytics.
|
||||
*/
|
||||
export function createRejoinedEvent({ url, lastConferenceDuration, timeSinceLeft }: {
|
||||
lastConferenceDuration: number;
|
||||
timeSinceLeft: number;
|
||||
url: string;
|
||||
}) {
|
||||
return {
|
||||
action: 'rejoined',
|
||||
attributes: {
|
||||
lastConferenceDuration,
|
||||
timeSinceLeft,
|
||||
url
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which specifies that the "confirm" button on the remote
|
||||
* mute dialog has been clicked.
|
||||
*
|
||||
* @param {string} participantId - The ID of the participant that was remotely
|
||||
* muted.
|
||||
* @param {string} mediaType - The media type of the channel to mute.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRemoteMuteConfirmedEvent(participantId: string, mediaType: string) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
attributes: {
|
||||
'participant_id': participantId,
|
||||
'media_type': mediaType
|
||||
},
|
||||
source: 'remote.mute.button',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that one of the buttons in the "remote
|
||||
* video menu" was clicked.
|
||||
*
|
||||
* @param {string} buttonName - The name of the button.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRemoteVideoMenuButtonEvent(buttonName: string, attributes = {}) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: buttonName,
|
||||
attributes,
|
||||
source: 'remote.video.menu',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The rtcstats websocket onclose event. We send this to amplitude in order
|
||||
* to detect trace ws prematurely closing.
|
||||
*
|
||||
* @param {Object} closeEvent - The event with which the websocket closed.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRTCStatsTraceCloseEvent(closeEvent: { code: string; reason: string; }) {
|
||||
const event: {
|
||||
action: string;
|
||||
code?: string;
|
||||
reason?: string;
|
||||
source: string;
|
||||
} = {
|
||||
action: 'trace.onclose',
|
||||
source: 'rtcstats'
|
||||
};
|
||||
|
||||
event.code = closeEvent.code;
|
||||
event.reason = closeEvent.reason;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event indicating that an action related to screen sharing
|
||||
* occurred (e.g. It was started or stopped).
|
||||
*
|
||||
* @param {string} action - The action which occurred.
|
||||
* @param {number?} value - The screenshare duration in seconds.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createScreenSharingEvent(action: string, value = null) {
|
||||
return {
|
||||
action,
|
||||
actionSubject: 'screen.sharing',
|
||||
attributes: {
|
||||
value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates the screen sharing video is not displayed when it needs to be displayed.
|
||||
*
|
||||
* @param {Object} attributes - Additional information that describes the issue.
|
||||
* @returns {Object} The event in a format suitable for sending via sendAnalytics.
|
||||
*/
|
||||
export function createScreenSharingIssueEvent(attributes = {}) {
|
||||
return {
|
||||
action: 'screen.sharing.issue',
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event associated with the "shared video" feature.
|
||||
*
|
||||
* @param {string} action - The action that the event represents.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createSharedVideoEvent(action: string, attributes = {}) {
|
||||
return {
|
||||
action,
|
||||
attributes,
|
||||
actionSubject: 'shared.video'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event associated with a shortcut being pressed, released or
|
||||
* triggered. By convention, where appropriate an attribute named 'enable'
|
||||
* should be used to indicate the action which resulted by the shortcut being
|
||||
* pressed (e.g. Whether screen sharing was enabled or disabled).
|
||||
*
|
||||
* @param {string} shortcut - The identifier of the shortcut which produced
|
||||
* an action.
|
||||
* @param {string} action - The action that the event represents (one
|
||||
* of ACTION_SHORTCUT_PRESSED, ACTION_SHORTCUT_RELEASED
|
||||
* or ACTION_SHORTCUT_TRIGGERED).
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @param {string} source - The event's source.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createShortcutEvent(
|
||||
shortcut: string,
|
||||
action = ACTION_SHORTCUT_TRIGGERED,
|
||||
attributes = {},
|
||||
source = 'keyboard.shortcut') {
|
||||
return {
|
||||
action,
|
||||
actionSubjectId: shortcut,
|
||||
attributes,
|
||||
source,
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates the "start audio only" configuration.
|
||||
*
|
||||
* @param {boolean} audioOnly - Whether "start audio only" is enabled or not.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createStartAudioOnlyEvent(audioOnly: boolean) {
|
||||
return {
|
||||
action: 'start.audio.only',
|
||||
attributes: {
|
||||
enabled: audioOnly
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates the "start silent" configuration.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createStartSilentEvent() {
|
||||
return {
|
||||
action: 'start.silent'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that HTMLAudioElement.play has failed.
|
||||
*
|
||||
* @param {string} elementID - The ID of the HTMLAudioElement.
|
||||
* @returns {Object} The event in a format suitable for sending via sendAnalytics.
|
||||
*/
|
||||
export function createAudioPlayErrorEvent(elementID: string) {
|
||||
return {
|
||||
action: 'audio.play.error',
|
||||
attributes: {
|
||||
elementID
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that HTMLAudioElement.play has succeeded after a prior failure.
|
||||
*
|
||||
* @param {string} elementID - The ID of the HTMLAudioElement.
|
||||
* @returns {Object} The event in a format suitable for sending via sendAnalytics.
|
||||
*/
|
||||
export function createAudioPlaySuccessEvent(elementID: string) {
|
||||
return {
|
||||
action: 'audio.play.success',
|
||||
attributes: {
|
||||
elementID
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates the "start muted" configuration.
|
||||
*
|
||||
* @param {string} source - The source of the configuration, 'local' or
|
||||
* 'remote' depending on whether it comes from the static configuration (i.e.
|
||||
* {@code config.js}) or comes dynamically from Jicofo.
|
||||
* @param {boolean} audioMute - Whether the configuration requests that audio
|
||||
* is muted.
|
||||
* @param {boolean} videoMute - Whether the configuration requests that video
|
||||
* is muted.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createStartMutedConfigurationEvent(
|
||||
source: string,
|
||||
audioMute: boolean,
|
||||
videoMute: boolean) {
|
||||
return {
|
||||
action: 'start.muted.configuration',
|
||||
attributes: {
|
||||
source,
|
||||
'audio_mute': audioMute,
|
||||
'video_mute': videoMute
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically changing the mute state of a media track in order to match
|
||||
* the current stored state in redux.
|
||||
*
|
||||
* @param {string} mediaType - The track's media type ('audio' or 'video').
|
||||
* @param {boolean} muted - Whether the track is being muted or unmuted as
|
||||
* as result of the sync operation.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createSyncTrackStateEvent(mediaType: string, muted: boolean) {
|
||||
return {
|
||||
action: 'sync.track.state',
|
||||
attributes: {
|
||||
'media_type': mediaType,
|
||||
muted
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event associated with a toolbar button being clicked/pressed. By
|
||||
* convention, where appropriate an attribute named 'enable' should be used to
|
||||
* indicate the action which resulted by the shortcut being pressed (e.g.
|
||||
* Whether screen sharing was enabled or disabled).
|
||||
*
|
||||
* @param {string} buttonName - The identifier of the toolbar button which was
|
||||
* clicked/pressed.
|
||||
* @param {Object} attributes - Attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createToolbarEvent(buttonName: string, attributes = {}) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: buttonName,
|
||||
attributes,
|
||||
source: 'toolbar.button',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event associated with a reaction button being clicked/pressed.
|
||||
*
|
||||
* @param {string} buttonName - The identifier of the reaction button which was
|
||||
* clicked/pressed.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createReactionMenuEvent(buttonName: string) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: 'button',
|
||||
source: 'reaction',
|
||||
buttonName,
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event associated with disabling of reaction sounds.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createReactionSoundsDisabledEvent() {
|
||||
return {
|
||||
action: 'disabled',
|
||||
actionSubject: 'sounds',
|
||||
source: 'reaction.settings',
|
||||
type: TYPE_UI
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that a local track was muted.
|
||||
*
|
||||
* @param {string} mediaType - The track's media type ('audio' or 'video').
|
||||
* @param {string} reason - The reason the track was muted (e.g. It was
|
||||
* triggered by the "initial mute" option, or a previously muted track was
|
||||
* replaced (e.g. When a new device was used)).
|
||||
* @param {boolean} muted - Whether the track was muted or unmuted.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createTrackMutedEvent(mediaType: string, reason: string, muted = true) {
|
||||
return {
|
||||
action: 'track.muted',
|
||||
attributes: {
|
||||
'media_type': mediaType,
|
||||
muted,
|
||||
reason
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for joining a vpaas conference.
|
||||
*
|
||||
* @param {string} tenant - The conference tenant.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createVpaasConferenceJoinedEvent(tenant: string) {
|
||||
return {
|
||||
action: 'vpaas.conference.joined',
|
||||
attributes: {
|
||||
tenant
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for an action on the welcome page.
|
||||
*
|
||||
* @param {string} action - The action that the event represents.
|
||||
* @param {string} actionSubject - The subject that was acted upon.
|
||||
* @param {boolean} attributes - Additional attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createWelcomePageEvent(action: string, actionSubject?: string, attributes = {}) {
|
||||
return {
|
||||
action,
|
||||
actionSubject,
|
||||
attributes,
|
||||
source: 'welcomePage'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates a screenshot of the screensharing has been taken.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createScreensharingCaptureTakenEvent() {
|
||||
return {
|
||||
action: 'screen.sharing.capture.taken'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for an action on breakout rooms.
|
||||
*
|
||||
* @param {string} actionSubject - The subject that was acted upon.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createBreakoutRoomsEvent(actionSubject: string) {
|
||||
return {
|
||||
action: 'clicked',
|
||||
actionSubject: `${actionSubject}.button`,
|
||||
source: 'breakout.rooms'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates a GIF was sent.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createGifSentEvent() {
|
||||
return {
|
||||
action: 'gif.sent'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates the whiteboard was opened.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createOpenWhiteboardEvent() {
|
||||
return {
|
||||
action: 'whiteboard.open'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates the whiteboard limit was enforced.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRestrictWhiteboardEvent() {
|
||||
return {
|
||||
action: 'whiteboard.restrict'
|
||||
};
|
||||
}
|
||||
29
react/features/analytics/actionTypes.ts
Normal file
29
react/features/analytics/actionTypes.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* The type of (redux) action which signals that local media duration has changed.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
* localTracksDuration: Object
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_LOCAL_TRACKS_DURATION = 'UPDATE_LOCAL_TRACKS_DURATION';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the isInitialized redux prop.
|
||||
*
|
||||
* {
|
||||
* type: SET_INITIALIZED,
|
||||
* value: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_INITIALIZED = 'SET_INITIALIZED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the initial permanent properties.
|
||||
*
|
||||
* {
|
||||
* type: SET_INITIAL_PERMANENT_PROPERTIES,
|
||||
* properties: Object
|
||||
* }
|
||||
*/
|
||||
export const SET_INITIAL_PERMANENT_PROPERTIES = 'SET_INITIAL_PERMANENT_PROPERTIES';
|
||||
25
react/features/analytics/actions.ts
Normal file
25
react/features/analytics/actions.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { analytics } from '../base/lib-jitsi-meet';
|
||||
|
||||
import { SET_INITIAL_PERMANENT_PROPERTIES } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Updates a permanentProperty.
|
||||
*
|
||||
* @param {Object} properties - An object with properties to be updated.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setPermanentProperty(properties: Object) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const { isInitialized = false } = getState()['features/analytics'];
|
||||
|
||||
if (isInitialized) {
|
||||
analytics.addPermanentProperties(properties);
|
||||
} else {
|
||||
dispatch({
|
||||
type: SET_INITIAL_PERMANENT_PROPERTIES,
|
||||
properties
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
339
react/features/analytics/functions.ts
Normal file
339
react/features/analytics/functions.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
// @ts-expect-error
|
||||
import { API_ID } from '../../../modules/API/constants';
|
||||
import { getName as getAppName } from '../app/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import { getAnalyticsRoomName } from '../base/conference/functions';
|
||||
import checkChromeExtensionsInstalled from '../base/environment/checkChromeExtensionsInstalled';
|
||||
import {
|
||||
isMobileBrowser
|
||||
} from '../base/environment/utils';
|
||||
import JitsiMeetJS, {
|
||||
analytics,
|
||||
browser
|
||||
} from '../base/lib-jitsi-meet';
|
||||
import { isAnalyticsEnabled } from '../base/lib-jitsi-meet/functions.any';
|
||||
import { isEmbedded } from '../base/util/embedUtils';
|
||||
import { getJitsiMeetGlobalNS } from '../base/util/helpers';
|
||||
import { loadScript } from '../base/util/loadScript';
|
||||
import { parseURLParams } from '../base/util/parseURLParams';
|
||||
import { parseURIString } from '../base/util/uri';
|
||||
import { isPrejoinPageVisible } from '../prejoin/functions';
|
||||
|
||||
import AmplitudeHandler from './handlers/AmplitudeHandler';
|
||||
import MatomoHandler from './handlers/MatomoHandler';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Sends an event through the lib-jitsi-meet AnalyticsAdapter interface.
|
||||
*
|
||||
* @param {Object} event - The event to send. It should be formatted as
|
||||
* described in AnalyticsAdapter.js in lib-jitsi-meet.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function sendAnalytics(event: Object) {
|
||||
try {
|
||||
analytics.sendEvent(event);
|
||||
} catch (e) {
|
||||
logger.warn(`Error sending analytics event: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return saved amplitude identity info such as session id, device id and user id. We assume these do not change for
|
||||
* the duration of the conference.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getAmplitudeIdentity() {
|
||||
return analytics.amplitudeIdentityProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the analytics adapter to its initial state - removes handlers, cache,
|
||||
* disabled state, etc.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function resetAnalytics() {
|
||||
analytics.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the analytics handlers.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action} is being dispatched.
|
||||
* @returns {Promise} Resolves with the handlers that have been successfully loaded.
|
||||
*/
|
||||
export async function createHandlers({ getState }: IStore) {
|
||||
getJitsiMeetGlobalNS().analyticsHandlers = [];
|
||||
|
||||
if (!isAnalyticsEnabled(getState)) {
|
||||
// Avoid all analytics processing if there are no handlers, since no event would be sent.
|
||||
analytics.dispose();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const config = state['features/base/config'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const host = locationURL ? locationURL.host : '';
|
||||
const {
|
||||
analytics: analyticsConfig = {},
|
||||
deploymentInfo
|
||||
} = config;
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
blackListedEvents,
|
||||
scriptURLs,
|
||||
matomoEndpoint,
|
||||
matomoSiteID,
|
||||
whiteListedEvents
|
||||
} = analyticsConfig;
|
||||
const { group, user } = state['features/base/jwt'];
|
||||
const handlerConstructorOptions = {
|
||||
amplitudeAPPKey,
|
||||
blackListedEvents,
|
||||
envType: deploymentInfo?.envType || 'dev',
|
||||
matomoEndpoint,
|
||||
matomoSiteID,
|
||||
group,
|
||||
host,
|
||||
product: deploymentInfo?.product,
|
||||
subproduct: deploymentInfo?.environment,
|
||||
user: user?.id,
|
||||
version: JitsiMeetJS.version,
|
||||
whiteListedEvents
|
||||
};
|
||||
const handlers = [];
|
||||
|
||||
if (amplitudeAPPKey) {
|
||||
try {
|
||||
const amplitude = new AmplitudeHandler(handlerConstructorOptions);
|
||||
|
||||
analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
|
||||
|
||||
handlers.push(amplitude);
|
||||
} catch (e) {
|
||||
logger.error('Failed to initialize Amplitude handler', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (matomoEndpoint && matomoSiteID) {
|
||||
try {
|
||||
const matomo = new MatomoHandler(handlerConstructorOptions);
|
||||
|
||||
handlers.push(matomo);
|
||||
} catch (e) {
|
||||
logger.error('Failed to initialize Matomo handler', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(scriptURLs) && scriptURLs.length > 0) {
|
||||
let externalHandlers;
|
||||
|
||||
try {
|
||||
externalHandlers = await _loadHandlers(scriptURLs, handlerConstructorOptions);
|
||||
handlers.push(...externalHandlers);
|
||||
} catch (e) {
|
||||
logger.error('Failed to initialize external analytics handlers', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid all analytics processing if there are no handlers, since no event would be sent.
|
||||
if (handlers.length === 0) {
|
||||
analytics.dispose();
|
||||
}
|
||||
|
||||
logger.info(`Initialized ${handlers.length} analytics handlers`);
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inits JitsiMeetJS.analytics by setting permanent properties and setting the handlers from the loaded scripts.
|
||||
* NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be null.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action} is being dispatched.
|
||||
* @param {Array<Object>} handlers - The analytics handlers.
|
||||
* @returns {boolean} - True if the analytics were successfully initialized and false otherwise.
|
||||
*/
|
||||
export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
if (!isAnalyticsEnabled(getState) || handlers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const config = state['features/base/config'];
|
||||
const {
|
||||
deploymentInfo
|
||||
} = config;
|
||||
const { group, server } = state['features/base/jwt'];
|
||||
const { locationURL = { href: '' } } = state['features/base/connection'];
|
||||
const { tenant } = parseURIString(locationURL.href) || {};
|
||||
const params = parseURLParams(locationURL.href) ?? {};
|
||||
const permanentProperties: {
|
||||
appName?: string;
|
||||
externalApi?: boolean;
|
||||
group?: string;
|
||||
inIframe?: boolean;
|
||||
isPromotedFromVisitor?: boolean;
|
||||
isVisitor?: boolean;
|
||||
overwritesCustomButtonsWithURL?: boolean;
|
||||
overwritesDefaultLogoUrl?: boolean;
|
||||
overwritesDeploymentUrls?: boolean;
|
||||
overwritesLiveStreamingUrls?: boolean;
|
||||
overwritesSupportUrl?: boolean;
|
||||
server?: string;
|
||||
tenant?: string;
|
||||
wasLobbyVisible?: boolean;
|
||||
wasPrejoinDisplayed?: boolean;
|
||||
websocket?: boolean;
|
||||
} & typeof deploymentInfo = {};
|
||||
|
||||
if (server) {
|
||||
permanentProperties.server = server;
|
||||
}
|
||||
if (group) {
|
||||
permanentProperties.group = group;
|
||||
}
|
||||
|
||||
// Report the application name
|
||||
permanentProperties.appName = getAppName();
|
||||
|
||||
// Report if user is using websocket
|
||||
permanentProperties.websocket = typeof config.websocket === 'string';
|
||||
|
||||
// Report if user is using the external API
|
||||
permanentProperties.externalApi = typeof API_ID === 'number';
|
||||
|
||||
// Report if we are loaded in iframe
|
||||
permanentProperties.inIframe = isEmbedded();
|
||||
|
||||
// Report the tenant from the URL.
|
||||
permanentProperties.tenant = tenant || '/';
|
||||
|
||||
permanentProperties.wasPrejoinDisplayed = isPrejoinPageVisible(state);
|
||||
|
||||
// Currently we don't know if there will be lobby. We will update it to true if we go through lobby.
|
||||
permanentProperties.wasLobbyVisible = false;
|
||||
|
||||
// Setting visitor properties to false by default. We will update them later if it turns out we are visitor.
|
||||
permanentProperties.isVisitor = false;
|
||||
permanentProperties.isPromotedFromVisitor = false;
|
||||
|
||||
// TODO: Temporary metric. To be removed once we don't need it.
|
||||
permanentProperties.overwritesSupportUrl = 'interfaceConfig.SUPPORT_URL' in params;
|
||||
permanentProperties.overwritesDefaultLogoUrl = 'config.defaultLogoUrl' in params;
|
||||
|
||||
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
|
||||
|
||||
permanentProperties.overwritesDeploymentUrls
|
||||
= 'config.deploymentUrls.downloadAppsUrl' in params || 'config.deploymentUrls.userDocumentationURL' in params
|
||||
|| (typeof deploymentUrlsConfig === 'object'
|
||||
&& ('downloadAppsUrl' in deploymentUrlsConfig || 'userDocumentationURL' in deploymentUrlsConfig));
|
||||
const liveStreamingConfig = params['config.liveStreaming'] ?? {};
|
||||
|
||||
permanentProperties.overwritesLiveStreamingUrls
|
||||
= ('interfaceConfig.LIVE_STREAMING_HELP_LINK' in params)
|
||||
|| ('config.liveStreaming.termsLink' in params)
|
||||
|| ('config.liveStreaming.dataPrivacyLink' in params)
|
||||
|| ('config.liveStreaming.helpLink' in params)
|
||||
|| (typeof params['config.liveStreaming'] === 'object' && 'config.liveStreaming' in params
|
||||
&& (
|
||||
'termsLink' in liveStreamingConfig
|
||||
|| 'dataPrivacyLink' in liveStreamingConfig
|
||||
|| 'helpLink' in liveStreamingConfig
|
||||
)
|
||||
);
|
||||
|
||||
permanentProperties.overwritesCustomButtonsWithURL = 'config.customToolbarButtons' in params;
|
||||
|
||||
// Optionally, include local deployment information based on the
|
||||
// contents of window.config.deploymentInfo.
|
||||
if (deploymentInfo) {
|
||||
for (const key in deploymentInfo) {
|
||||
if (deploymentInfo.hasOwnProperty(key)) {
|
||||
permanentProperties[key as keyof typeof deploymentInfo] = deploymentInfo[
|
||||
key as keyof typeof deploymentInfo];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analytics.addPermanentProperties({
|
||||
...permanentProperties,
|
||||
...getState()['features/analytics'].initialPermanentProperties
|
||||
});
|
||||
|
||||
analytics.setConferenceName(getAnalyticsRoomName(state, dispatch));
|
||||
|
||||
// Set the handlers last, since this triggers emptying of the cache
|
||||
analytics.setAnalyticsHandlers(handlers);
|
||||
|
||||
if (!isMobileBrowser() && browser.isChromiumBased()) {
|
||||
const bannerCfg = state['features/base/config'].chromeExtensionBanner;
|
||||
|
||||
checkChromeExtensionsInstalled(bannerCfg).then(extensionsInstalled => {
|
||||
if (extensionsInstalled?.length) {
|
||||
analytics.addPermanentProperties({
|
||||
hasChromeExtension: extensionsInstalled.some(ext => ext)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load the scripts for the external analytics handlers and creates them.
|
||||
*
|
||||
* @param {Array} scriptURLs - The array of script urls to load.
|
||||
* @param {Object} handlerConstructorOptions - The default options to pass when creating handlers.
|
||||
* @private
|
||||
* @returns {Promise} Resolves with the handlers that have been successfully loaded and rejects if there are no handlers
|
||||
* loaded or the analytics is disabled.
|
||||
*/
|
||||
function _loadHandlers(scriptURLs: string[] = [], handlerConstructorOptions: Object) {
|
||||
const promises: Promise<{ error?: Error; type: string; url?: string; }>[] = [];
|
||||
|
||||
for (const url of scriptURLs) {
|
||||
promises.push(
|
||||
loadScript(url).then(
|
||||
() => {
|
||||
return { type: 'success' };
|
||||
},
|
||||
(error: Error) => {
|
||||
return {
|
||||
type: 'error',
|
||||
error,
|
||||
url
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(values => {
|
||||
for (const el of values) {
|
||||
if (el.type === 'error') {
|
||||
logger.warn(`Failed to load ${el.url}: ${el.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const handlers = [];
|
||||
|
||||
for (const Handler of getJitsiMeetGlobalNS().analyticsHandlers) {
|
||||
// Catch any error while loading to avoid skipping analytics in case
|
||||
// of multiple scripts.
|
||||
try {
|
||||
handlers.push(new Handler(handlerConstructorOptions));
|
||||
} catch (error) {
|
||||
logger.warn(`Error creating analytics handler: ${error}`);
|
||||
}
|
||||
}
|
||||
logger.debug(`Loaded ${handlers.length} external analytics handlers`);
|
||||
|
||||
return handlers;
|
||||
});
|
||||
}
|
||||
115
react/features/analytics/handlers/AbstractHandler.ts
Normal file
115
react/features/analytics/handlers/AbstractHandler.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
export interface IEvent {
|
||||
action?: string;
|
||||
actionSubject?: string;
|
||||
attributes?: {
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
name?: string;
|
||||
source?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface IOptions {
|
||||
amplitudeAPPKey?: string;
|
||||
blackListedEvents?: string[];
|
||||
envType?: string;
|
||||
group?: string;
|
||||
host?: string;
|
||||
matomoEndpoint?: string;
|
||||
matomoSiteID?: string;
|
||||
product?: string;
|
||||
subproduct?: string;
|
||||
user?: string;
|
||||
version?: string;
|
||||
whiteListedEvents?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract implementation of analytics handler.
|
||||
*/
|
||||
export default class AbstractHandler {
|
||||
_enabled: boolean;
|
||||
_whiteListedEvents: Array<string> | undefined;
|
||||
_blackListedEvents: Array<string> | undefined;
|
||||
|
||||
/**
|
||||
* Creates new instance.
|
||||
*
|
||||
* @param {Object} options - Optional parameters.
|
||||
*/
|
||||
constructor(options: IOptions = {}) {
|
||||
this._enabled = false;
|
||||
this._whiteListedEvents = options.whiteListedEvents;
|
||||
|
||||
// FIXME:
|
||||
// Keeping the list with the very noisy events so that we don't flood with events whoever hasn't configured
|
||||
// white/black lists yet. We need to solve this issue properly by either making these events not so noisy or
|
||||
// by removing them completely from the code.
|
||||
this._blackListedEvents = [
|
||||
...(options.blackListedEvents || []), // eslint-disable-line no-extra-parens
|
||||
'e2e_rtt', 'rtp.stats', 'rtt.by.region', 'available.device', 'stream.switch.delay', 'ice.state.changed',
|
||||
'ice.duration', 'peer.conn.status.duration'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a name for the event from the event properties.
|
||||
*
|
||||
* @param {Object} event - The analytics event.
|
||||
* @returns {string} - The extracted name.
|
||||
*/
|
||||
_extractName(event: IEvent) {
|
||||
// Page events have a single 'name' field.
|
||||
if (event.type === 'page') {
|
||||
return event.name;
|
||||
}
|
||||
|
||||
const {
|
||||
action,
|
||||
actionSubject,
|
||||
source
|
||||
} = event;
|
||||
|
||||
// All events have action, actionSubject, and source fields. All
|
||||
// three fields are required, and often jitsi-meet and
|
||||
// lib-jitsi-meet use the same value when separate values are not
|
||||
// necessary (i.e. event.action == event.actionSubject).
|
||||
// Here we concatenate these three fields, but avoid adding the same
|
||||
// value twice, because it would only make the event's name harder
|
||||
// to read.
|
||||
let name = action;
|
||||
|
||||
if (actionSubject && actionSubject !== action) {
|
||||
name = `${actionSubject}.${action}`;
|
||||
}
|
||||
if (source && source !== action) {
|
||||
name = `${source}.${name}`;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an event should be ignored or not.
|
||||
*
|
||||
* @param {Object} event - The event.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_shouldIgnore(event: IEvent) {
|
||||
if (!event || !this._enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const name = this._extractName(event) ?? '';
|
||||
|
||||
if (Array.isArray(this._whiteListedEvents)) {
|
||||
return this._whiteListedEvents.indexOf(name) === -1;
|
||||
}
|
||||
|
||||
if (Array.isArray(this._blackListedEvents)) {
|
||||
return this._blackListedEvents.indexOf(name) !== -1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
91
react/features/analytics/handlers/AmplitudeHandler.ts
Normal file
91
react/features/analytics/handlers/AmplitudeHandler.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Identify } from '@amplitude/analytics-core';
|
||||
|
||||
import logger from '../logger';
|
||||
|
||||
import AbstractHandler, { IEvent } from './AbstractHandler';
|
||||
import { fixDeviceID } from './amplitude/fixDeviceID';
|
||||
import amplitude, { initAmplitude } from './amplitude/lib';
|
||||
|
||||
/**
|
||||
* Analytics handler for Amplitude.
|
||||
*/
|
||||
export default class AmplitudeHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* Creates new instance of the Amplitude analytics handler.
|
||||
*
|
||||
* @param {Object} options - The amplitude options.
|
||||
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API
|
||||
* in the Amplitude events.
|
||||
*/
|
||||
constructor(options: any) {
|
||||
super(options);
|
||||
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
user
|
||||
} = options;
|
||||
|
||||
this._enabled = true;
|
||||
|
||||
initAmplitude(amplitudeAPPKey, user)
|
||||
.then(() => {
|
||||
logger.info('Amplitude initialized');
|
||||
fixDeviceID(amplitude);
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error('Error initializing Amplitude', e);
|
||||
this._enabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Amplitude user properties.
|
||||
*
|
||||
* @param {Object} userProps - The user properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserProperties(userProps: any) {
|
||||
if (this._enabled) {
|
||||
const identify = new Identify();
|
||||
|
||||
// Set all properties
|
||||
Object.entries(userProps).forEach(([ key, value ]) => {
|
||||
identify.set(key, value as any);
|
||||
});
|
||||
|
||||
amplitude.identify(identify);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an event to Amplitude. The format of the event is described
|
||||
* in AnalyticsAdapter in lib-jitsi-meet.
|
||||
*
|
||||
* @param {Object} event - The event in the format specified by
|
||||
* lib-jitsi-meet.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendEvent(event: IEvent) {
|
||||
if (this._shouldIgnore(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventName = this._extractName(event) ?? '';
|
||||
|
||||
amplitude.track(eventName, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return amplitude identity information.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getIdentityProps() {
|
||||
return {
|
||||
sessionId: amplitude.getSessionId(),
|
||||
deviceId: amplitude.getDeviceId(),
|
||||
userId: amplitude.getUserId()
|
||||
};
|
||||
}
|
||||
}
|
||||
170
react/features/analytics/handlers/MatomoHandler.ts
Normal file
170
react/features/analytics/handlers/MatomoHandler.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/* global _paq */
|
||||
|
||||
import { getJitsiMeetGlobalNS } from '../../base/util/helpers';
|
||||
|
||||
import AbstractHandler, { IEvent } from './AbstractHandler';
|
||||
|
||||
/**
|
||||
* Analytics handler for Matomo.
|
||||
*/
|
||||
export default class MatomoHandler extends AbstractHandler {
|
||||
_userProperties: Object;
|
||||
|
||||
/**
|
||||
* Creates new instance of the Matomo handler.
|
||||
*
|
||||
* @param {Object} options - The matomo options.
|
||||
* @param {string} options.matomoEndpoint - The Matomo endpoint.
|
||||
* @param {string} options.matomoSiteID - The site ID.
|
||||
*/
|
||||
constructor(options: any) {
|
||||
super(options);
|
||||
this._userProperties = {};
|
||||
|
||||
if (!options.matomoEndpoint) {
|
||||
throw new Error(
|
||||
'Failed to initialize Matomo handler: no endpoint defined.'
|
||||
);
|
||||
}
|
||||
if (!options.matomoSiteID) {
|
||||
throw new Error(
|
||||
'Failed to initialize Matomo handler: no site ID defined.'
|
||||
);
|
||||
}
|
||||
|
||||
this._enabled = true;
|
||||
this._initMatomo(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the _paq object.
|
||||
*
|
||||
* @param {Object} options - The matomo options.
|
||||
* @param {string} options.matomoEndpoint - The Matomo endpoint.
|
||||
* @param {string} options.matomoSiteID - The site ID.
|
||||
* @returns {void}
|
||||
*/
|
||||
_initMatomo(options: any) {
|
||||
// @ts-ignore
|
||||
const _paq = window._paq || [];
|
||||
|
||||
// @ts-ignore
|
||||
window._paq = _paq;
|
||||
|
||||
_paq.push([ 'trackPageView' ]);
|
||||
_paq.push([ 'enableLinkTracking' ]);
|
||||
|
||||
(function() {
|
||||
// add trailing slash if needed
|
||||
const u = options.matomoEndpoint.endsWith('/')
|
||||
? options.matomoEndpoint
|
||||
: `${options.matomoEndpoint}/`;
|
||||
|
||||
// configure the tracker
|
||||
_paq.push([ 'setTrackerUrl', `${u}matomo.php` ]);
|
||||
_paq.push([ 'setSiteId', options.matomoSiteID ]);
|
||||
|
||||
// insert the matomo script
|
||||
const d = document,
|
||||
g = d.createElement('script'),
|
||||
s = d.getElementsByTagName('script')[0];
|
||||
|
||||
g.type = 'text/javascript';
|
||||
g.async = true;
|
||||
g.defer = true;
|
||||
g.src = `${u}matomo.js`;
|
||||
s.parentNode?.insertBefore(g, s);
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the integer to use for a Matomo event's value field
|
||||
* from a lib-jitsi-meet analytics event.
|
||||
*
|
||||
* @param {Object} event - The lib-jitsi-meet analytics event.
|
||||
* @returns {number} - The integer to use for the 'value' of a Matomo
|
||||
* event, or NaN if the lib-jitsi-meet event doesn't contain a
|
||||
* suitable value.
|
||||
* @private
|
||||
*/
|
||||
_extractValue(event: IEvent) {
|
||||
const value = event?.attributes?.value;
|
||||
|
||||
// Try to extract an integer from the 'value' attribute.
|
||||
return Math.round(parseFloat(value ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the permanent properties for the current session.
|
||||
*
|
||||
* @param {Object} userProps - The permanent properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserProperties(userProps: any = {}) {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visitScope = [ 'user_agent', 'callstats_name', 'browser_name' ];
|
||||
|
||||
// add variables in the 'page' scope
|
||||
Object.keys(userProps)
|
||||
.filter(key => visitScope.indexOf(key) === -1)
|
||||
.forEach((key, index) => {
|
||||
// @ts-ignore
|
||||
_paq.push([
|
||||
'setCustomVariable',
|
||||
1 + index,
|
||||
key,
|
||||
userProps[key],
|
||||
'page'
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// add variables in the 'visit' scope
|
||||
Object.keys(userProps)
|
||||
.filter(key => visitScope.indexOf(key) !== -1)
|
||||
.forEach((key, index) => {
|
||||
// @ts-ignore
|
||||
_paq.push([
|
||||
'setCustomVariable',
|
||||
1 + index,
|
||||
key,
|
||||
userProps[key],
|
||||
'visit'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the entry point of the API. The function sends an event to
|
||||
* the Matomo endpoint. The format of the event is described in
|
||||
* analyticsAdapter in lib-jitsi-meet.
|
||||
*
|
||||
* @param {Object} event - The event in the format specified by
|
||||
* lib-jitsi-meet.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendEvent(event: IEvent) {
|
||||
if (this._shouldIgnore(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this._extractValue(event);
|
||||
const matomoEvent: Array<string | number | undefined> = [
|
||||
'trackEvent', 'jitsi-meet', this._extractName(event) ];
|
||||
|
||||
if (!isNaN(value)) {
|
||||
matomoEvent.push(value);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
_paq.push(matomoEvent);
|
||||
}
|
||||
}
|
||||
|
||||
const globalNS = getJitsiMeetGlobalNS();
|
||||
|
||||
globalNS.analyticsHandlers = globalNS.analyticsHandlers || [];
|
||||
globalNS.analyticsHandlers.push(MatomoHandler);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Types } from '@amplitude/analytics-react-native';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
import { getUniqueId } from 'react-native-device-info';
|
||||
|
||||
import logger from '../../logger';
|
||||
|
||||
|
||||
/**
|
||||
* Custom logic for setting the correct device id.
|
||||
*
|
||||
* @param {Types.ReactNativeClient} amplitude - The amplitude instance.
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
|
||||
await DefaultPreference.setName('jitsi-preferences');
|
||||
|
||||
const current = await DefaultPreference.get('amplitudeDeviceId');
|
||||
|
||||
if (current) {
|
||||
amplitude.setDeviceId(current);
|
||||
} else {
|
||||
const uid = await getUniqueId();
|
||||
|
||||
if (!uid) {
|
||||
logger.warn('Device ID is not set!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
amplitude.setDeviceId(uid as string);
|
||||
await DefaultPreference.set('amplitudeDeviceId', uid as string);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Types } from '@amplitude/analytics-browser';
|
||||
// @ts-ignore
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
* Key used to store the device id in local storage.
|
||||
*/
|
||||
const DEVICE_ID_KEY = '__AMDID';
|
||||
|
||||
/**
|
||||
* Custom logic for setting the correct device id.
|
||||
*
|
||||
* @param {Types.BrowserClient} amplitude - The amplitude instance.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function fixDeviceID(amplitude: Types.BrowserClient) {
|
||||
const deviceId = jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
|
||||
if (deviceId) {
|
||||
// Set the device id in Amplitude.
|
||||
try {
|
||||
amplitude.setDeviceId(JSON.parse(deviceId));
|
||||
} catch (error) {
|
||||
logger.error('Failed to set device ID in Amplitude', error);
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
} else {
|
||||
const newDeviceId = amplitude.getDeviceId();
|
||||
|
||||
if (newDeviceId) {
|
||||
jitsiLocalStorage.setItem(DEVICE_ID_KEY, JSON.stringify(newDeviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amplitude shared deviceId.
|
||||
*
|
||||
* @returns {string} - The amplitude deviceId.
|
||||
*/
|
||||
export function getDeviceID() {
|
||||
return jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
}
|
||||
15
react/features/analytics/handlers/amplitude/lib.native.ts
Normal file
15
react/features/analytics/handlers/amplitude/lib.native.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import amplitude from '@amplitude/analytics-react-native';
|
||||
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
return amplitude.init(amplitudeAPPKey, user, {}).promise;
|
||||
}
|
||||
38
react/features/analytics/handlers/amplitude/lib.web.ts
Normal file
38
react/features/analytics/handlers/amplitude/lib.web.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createInstance } from '@amplitude/analytics-browser';
|
||||
|
||||
const amplitude = createInstance();
|
||||
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
|
||||
// Forces sending all events on exit (flushing) via sendBeacon.
|
||||
window.addEventListener('pagehide', () => {
|
||||
// Set https transport to use sendBeacon API.
|
||||
amplitude.setTransport('beacon');
|
||||
// Send all pending events to server.
|
||||
amplitude.flush();
|
||||
});
|
||||
|
||||
const options = {
|
||||
autocapture: {
|
||||
attribution: true,
|
||||
pageViews: true,
|
||||
sessions: false,
|
||||
fileDownloads: false,
|
||||
formInteractions: false,
|
||||
elementInteractions: false
|
||||
},
|
||||
defaultTracking: false
|
||||
};
|
||||
|
||||
return amplitude.init(amplitudeAPPKey, user, options).promise;
|
||||
}
|
||||
3
react/features/analytics/logger.ts
Normal file
3
react/features/analytics/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/analytics');
|
||||
216
react/features/analytics/middleware.ts
Normal file
216
react/features/analytics/middleware.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
SET_ROOM
|
||||
} from '../base/conference/actionTypes';
|
||||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import { SET_NETWORK_INFO } from '../base/net-info/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import {
|
||||
TRACK_ADDED,
|
||||
TRACK_REMOVED,
|
||||
TRACK_UPDATED
|
||||
} from '../base/tracks/actionTypes';
|
||||
import {
|
||||
getLocalAudioTrack,
|
||||
getLocalVideoTrack
|
||||
} from '../base/tracks/functions';
|
||||
import { SET_LOBBY_VISIBILITY } from '../lobby/actionTypes';
|
||||
import { getIsLobbyVisible } from '../lobby/functions';
|
||||
import { I_AM_VISITOR_MODE } from '../visitors/actionTypes';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
import { createLocalTracksDurationEvent, createNetworkInfoEvent } from './AnalyticsEvents';
|
||||
import { SET_INITIALIZED, UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes';
|
||||
import { setPermanentProperty } from './actions';
|
||||
import { createHandlers, initAnalytics, resetAnalytics, sendAnalytics } from './functions';
|
||||
|
||||
/**
|
||||
* Calculates the duration of the local tracks.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object} - The local tracks duration.
|
||||
*/
|
||||
function calculateLocalTrackDuration(state: IReduxState) {
|
||||
const now = Date.now();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { audio, video } = localTracksDuration;
|
||||
const { camera, desktop } = video;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const audioTrack = getLocalAudioTrack(tracks);
|
||||
const videoTrack = getLocalVideoTrack(tracks);
|
||||
const newDuration = { ...localTracksDuration };
|
||||
|
||||
if (!audioTrack || audioTrack.muted || !conference) {
|
||||
newDuration.audio = {
|
||||
startedTime: -1,
|
||||
value: audio.value + (audio.startedTime === -1 ? 0 : now - audio.startedTime)
|
||||
};
|
||||
} else if (audio.startedTime === -1) {
|
||||
newDuration.audio.startedTime = now;
|
||||
}
|
||||
|
||||
if (!videoTrack || videoTrack.muted || !conference) {
|
||||
newDuration.video = {
|
||||
camera: {
|
||||
startedTime: -1,
|
||||
value: camera.value + (camera.startedTime === -1 ? 0 : now - camera.startedTime)
|
||||
},
|
||||
desktop: {
|
||||
startedTime: -1,
|
||||
value: desktop.value + (desktop.startedTime === -1 ? 0 : now - desktop.startedTime)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const { videoType } = videoTrack;
|
||||
|
||||
if (video[videoType as keyof typeof video].startedTime === -1) {
|
||||
newDuration.video[videoType as keyof typeof video].startedTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...localTracksDuration,
|
||||
...newDuration
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware which intercepts config actions to handle evaluating analytics
|
||||
* config based on the config stored in the store.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case I_AM_VISITOR_MODE: {
|
||||
const oldIAmVisitor = iAmVisitor(store.getState());
|
||||
const result = next(action);
|
||||
const newIAmVisitor = iAmVisitor(store.getState());
|
||||
|
||||
store.dispatch(setPermanentProperty({
|
||||
isVisitor: newIAmVisitor,
|
||||
isPromotedFromVisitor: oldIAmVisitor && !newIAmVisitor
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
case SET_CONFIG:
|
||||
if (navigator.product === 'ReactNative') {
|
||||
// Resetting the analytics is currently not needed for web because
|
||||
// the user will be redirected to another page and new instance of
|
||||
// Analytics will be created and initialized.
|
||||
resetAnalytics();
|
||||
|
||||
const { dispatch } = store;
|
||||
|
||||
dispatch({
|
||||
type: SET_INITIALIZED,
|
||||
value: false
|
||||
});
|
||||
}
|
||||
break;
|
||||
case SET_ROOM: {
|
||||
// createHandlers is called before the SET_ROOM action is executed in order for Amplitude to initialize before
|
||||
// the deeplinking logic is executed (after the SET_ROOM action) so that the Amplitude device id is available
|
||||
// if needed.
|
||||
const createHandlersPromise = createHandlers(store);
|
||||
const result = next(action);
|
||||
|
||||
createHandlersPromise.then(handlers => {
|
||||
if (initAnalytics(store, handlers)) {
|
||||
store.dispatch({
|
||||
type: SET_INITIALIZED,
|
||||
value: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: {
|
||||
...calculateLocalTrackDuration(state),
|
||||
conference: {
|
||||
startedTime: Date.now(),
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_WILL_LEAVE: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
const newLocalTracksDuration = {
|
||||
...calculateLocalTrackDuration(state),
|
||||
conference: {
|
||||
startedTime: -1,
|
||||
value: Date.now() - localTracksDuration.conference.startedTime
|
||||
}
|
||||
};
|
||||
|
||||
sendAnalytics(createLocalTracksDurationEvent(newLocalTracksDuration));
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: newLocalTracksDuration
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SET_LOBBY_VISIBILITY:
|
||||
if (getIsLobbyVisible(store.getState())) {
|
||||
store.dispatch(setPermanentProperty({
|
||||
wasLobbyVisible: true
|
||||
}));
|
||||
}
|
||||
|
||||
break;
|
||||
case SET_NETWORK_INFO:
|
||||
sendAnalytics(
|
||||
createNetworkInfoEvent({
|
||||
isOnline: action.isOnline,
|
||||
details: action.details,
|
||||
networkType: action.networkType
|
||||
}));
|
||||
break;
|
||||
case TRACK_ADDED:
|
||||
case TRACK_REMOVED:
|
||||
case TRACK_UPDATED: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
|
||||
if (localTracksDuration.conference.startedTime === -1) {
|
||||
// We don't want to track the media duration if the conference is not joined yet because otherwise we won't
|
||||
// be able to compare them with the conference duration (from conference join to conference will leave).
|
||||
break;
|
||||
}
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: {
|
||||
...localTracksDuration,
|
||||
...calculateLocalTrackDuration(state)
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
88
react/features/analytics/reducer.ts
Normal file
88
react/features/analytics/reducer.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
SET_INITIALIZED,
|
||||
SET_INITIAL_PERMANENT_PROPERTIES,
|
||||
UPDATE_LOCAL_TRACKS_DURATION
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Initial state.
|
||||
*/
|
||||
const DEFAULT_STATE = {
|
||||
isInitialized: false,
|
||||
initialPermanentProperties: {},
|
||||
localTracksDuration: {
|
||||
audio: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
},
|
||||
video: {
|
||||
camera: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
},
|
||||
desktop: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
}
|
||||
},
|
||||
conference: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface IValue {
|
||||
startedTime: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface IAnalyticsState {
|
||||
initialPermanentProperties: Object;
|
||||
isInitialized: boolean;
|
||||
localTracksDuration: {
|
||||
audio: IValue;
|
||||
conference: IValue;
|
||||
video: {
|
||||
camera: IValue;
|
||||
desktop: IValue;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for actions which changes the state of the analytics feature.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature features/analytics.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register<IAnalyticsState>('features/analytics',
|
||||
(state = DEFAULT_STATE, action): IAnalyticsState => {
|
||||
switch (action.type) {
|
||||
case SET_INITIALIZED:
|
||||
return {
|
||||
...state,
|
||||
initialPermanentProperties: action.value ? state.initialPermanentProperties : {},
|
||||
isInitialized: action.value
|
||||
};
|
||||
case SET_INITIAL_PERMANENT_PROPERTIES:
|
||||
return {
|
||||
...state,
|
||||
initialPermanentProperties: {
|
||||
...state.initialPermanentProperties,
|
||||
...action.properties
|
||||
}
|
||||
};
|
||||
case UPDATE_LOCAL_TRACKS_DURATION:
|
||||
return {
|
||||
...state,
|
||||
localTracksDuration: action.localTracksDuration
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user