feat: Track user time in meetings (#10)

* feat: Track time users are in meetings

* chore: Remove unnecessary overrides from eslint config and console.log

* refactor: Separate the event into two: connect & disconnect

* chore: Add Segment api keys

* feat: Track disconnection only once and pass only world name and content server url
This commit is contained in:
Kevin Szuchet 2023-08-18 15:56:28 +02:00 committed by GitHub
parent 89ebf35e6b
commit 02f1a6df5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 122 additions and 68 deletions

38
package-lock.json generated
View File

@ -19,7 +19,7 @@
"ajv": "^8.12.0",
"classnames": "^2.3.2",
"decentraland-dapps": "^15.5.0",
"decentraland-ui": "^4.0.0",
"decentraland-ui": "^4.6.0",
"ethers": "^6.6.5",
"livekit-client": "^1.12.3",
"livekit-server-sdk": "^1.2.5",
@ -8346,11 +8346,11 @@
"integrity": "sha512-L4/bPD2fOeEdtFx+OnO3N81+/gsOkdensIuV9uFGYSN1mSTFaxHkWkhG8DOZ/8jlD0H2Qjkj6yDcWFaK+qu1Dg=="
},
"node_modules/decentraland-ui": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/decentraland-ui/-/decentraland-ui-4.1.0.tgz",
"integrity": "sha512-lK96yfPhQMusmaiIyhcL4O4nlubRcEq9kem59NBT3O7/gP4O9QLMLoNItZ8gG1U+4Z3FdMLMZ1+/vxq/UMdQTA==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/decentraland-ui/-/decentraland-ui-4.6.0.tgz",
"integrity": "sha512-dGa0aXpIGUaHTD9T9y7JRXT9JpQtgpwHWmpJWJWeG8A3yUUlfJwdIayLEATmtUpYeCGDJGNZhx4ctgUjoV7Y/Q==",
"dependencies": {
"@dcl/schemas": "^8.1.0",
"@dcl/schemas": "^9.2.0",
"balloon-css": "^0.5.0",
"classnames": "^2.3.2",
"deep-equal": "^2.0.5",
@ -8358,8 +8358,8 @@
"events": "^3.3.0",
"fp-future": "^1.0.1",
"parallax-js": "^3.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0",
"react-responsive": "^9.0.0-beta.3",
"react-semantic-ui-datepickers": "^2.17.2",
"react-tile-map": "^0.4.1",
@ -8381,9 +8381,9 @@
}
},
"node_modules/decentraland-ui/node_modules/@dcl/schemas": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-8.1.0.tgz",
"integrity": "sha512-1A7st/fESASmss+T1Vxc9lo3LHqSvgfDTLtLD2uhdgtOS0RombNES3KG/2zDfBpg9v3DCEchN6i7mWiCEw7/6g==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-9.2.0.tgz",
"integrity": "sha512-OGjjfbL+JsTqphvc6Msl4a5kYCGJlq2LmAMBY9WDlYRE+9DZBE2Sin+ebekYWBmSUA75mJHO+qUpg0/FC8z+oQ==",
"dependencies": {
"ajv": "^8.11.0",
"ajv-errors": "^3.0.0",
@ -26176,11 +26176,11 @@
"integrity": "sha512-L4/bPD2fOeEdtFx+OnO3N81+/gsOkdensIuV9uFGYSN1mSTFaxHkWkhG8DOZ/8jlD0H2Qjkj6yDcWFaK+qu1Dg=="
},
"decentraland-ui": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/decentraland-ui/-/decentraland-ui-4.1.0.tgz",
"integrity": "sha512-lK96yfPhQMusmaiIyhcL4O4nlubRcEq9kem59NBT3O7/gP4O9QLMLoNItZ8gG1U+4Z3FdMLMZ1+/vxq/UMdQTA==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/decentraland-ui/-/decentraland-ui-4.6.0.tgz",
"integrity": "sha512-dGa0aXpIGUaHTD9T9y7JRXT9JpQtgpwHWmpJWJWeG8A3yUUlfJwdIayLEATmtUpYeCGDJGNZhx4ctgUjoV7Y/Q==",
"requires": {
"@dcl/schemas": "^8.1.0",
"@dcl/schemas": "^9.2.0",
"balloon-css": "^0.5.0",
"classnames": "^2.3.2",
"deep-equal": "^2.0.5",
@ -26188,8 +26188,8 @@
"events": "^3.3.0",
"fp-future": "^1.0.1",
"parallax-js": "^3.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0",
"react-responsive": "^9.0.0-beta.3",
"react-semantic-ui-datepickers": "^2.17.2",
"react-tile-map": "^0.4.1",
@ -26199,9 +26199,9 @@
},
"dependencies": {
"@dcl/schemas": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-8.1.0.tgz",
"integrity": "sha512-1A7st/fESASmss+T1Vxc9lo3LHqSvgfDTLtLD2uhdgtOS0RombNES3KG/2zDfBpg9v3DCEchN6i7mWiCEw7/6g==",
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-9.2.0.tgz",
"integrity": "sha512-OGjjfbL+JsTqphvc6Msl4a5kYCGJlq2LmAMBY9WDlYRE+9DZBE2Sin+ebekYWBmSUA75mJHO+qUpg0/FC8z+oQ==",
"requires": {
"ajv": "^8.11.0",
"ajv-errors": "^3.0.0",

View File

@ -28,7 +28,7 @@
"ajv": "^8.12.0",
"classnames": "^2.3.2",
"decentraland-dapps": "^15.5.0",
"decentraland-ui": "^4.0.0",
"decentraland-ui": "^4.6.0",
"ethers": "^6.6.5",
"livekit-client": "^1.12.3",
"livekit-server-sdk": "^1.2.5",

View File

@ -4,8 +4,8 @@ import type { SVGProps } from 'react'
const PeopleIcon = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="25" viewBox="0 0 28 25" fill="none" {...props}>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M10.0003 11.1667C12.9417 11.1667 15.3337 8.77467 15.3337 5.83333C15.3337 2.892 12.9417 0.5 10.0003 0.5C7.05899 0.5 4.66699 2.892 4.66699 5.83333C4.66699 8.77467 7.05899 11.1667 10.0003 11.1667ZM20.667 13.8333C22.8723 13.8333 24.667 12.0387 24.667 9.83333C24.667 7.628 22.8723 5.83333 20.667 5.83333C18.4617 5.83333 16.667 7.628 16.667 9.83333C16.667 12.0387 18.4617 13.8333 20.667 13.8333ZM27.3337 21.8333C27.3337 22.5693 26.7377 23.1667 26.0003 23.1667H19.3337C19.3337 23.9027 18.7377 24.5 18.0003 24.5H2.00033C1.26299 24.5 0.666992 23.9027 0.666992 23.1667C0.666992 18.02 4.85499 13.8333 10.0003 13.8333C12.5697 13.8333 14.8977 14.8773 16.587 16.5613C17.7457 15.6653 19.1723 15.1667 20.667 15.1667C24.343 15.1667 27.3337 18.1573 27.3337 21.8333Z"
fill={props.fill ?? 'white'}
/>

View File

@ -1,24 +1,19 @@
import { connect } from 'react-redux'
import { getAddress, isConnecting } from 'decentraland-dapps/dist/modules/wallet/selectors'
import { getServer, getToken } from '../../../modules/conference/selector'
import { getServer, getToken, getWorldContentServerUrl, getWorldName } from '../../../modules/conference/selector'
import { isLoggingIn } from '../../../modules/identity/selector'
import { RootState } from '../../../modules/reducer'
import withRouter from '../../../utils/WithRouter'
import Conference from './Conference'
import { MapDispatch, MapStateProps, OwnProps } from './Conference.types'
import { MapStateProps } from './Conference.types'
const mapStateToProps = (state: RootState, ownProps: OwnProps): MapStateProps => {
const addressFromPath = ownProps.router.params.profileAddress
const mapStateToProps = (state: RootState): MapStateProps => ({
isLoading: isLoggingIn(state) || isConnecting(state),
loggedInAddress: getAddress(state)?.toLowerCase(),
server: getServer(state),
token: getToken(state),
worldName: getWorldName(state),
worldContentServerUrl: getWorldContentServerUrl(state)
})
return {
profileAddress: addressFromPath?.toLowerCase(),
isLoading: isLoggingIn(state) || isConnecting(state),
loggedInAddress: getAddress(state)?.toLowerCase(),
server: getServer(state),
token: getToken(state)
}
}
const mapDispatch = (_dispatch: MapDispatch): any => ({})
export default withRouter(connect(mapStateToProps, mapDispatch)(Conference))
export default withRouter(connect(mapStateToProps)(Conference))

View File

@ -1,16 +1,58 @@
import React from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { LiveKitRoom } from '@livekit/components-react'
import '@livekit/components-styles'
import { getAnalytics } from 'decentraland-dapps/dist/modules/analytics/utils'
import { Events } from '../../../modules/analytics/types'
import { VideoConference } from '../../VideoConference/'
import { Props } from './Conference.types'
import './Conference.css'
export default function Conference(props: Props) {
const { token, server } = props
const { token, server, worldName, worldContentServerUrl } = props
const [alreadyDisconnected, setAlreadyDisconnected] = useState(false)
const analytics = getAnalytics()
const track = useCallback(
(event: Events) => {
if (!worldName || !worldContentServerUrl) return
analytics.track(event, {
worldName,
worldContentServerUrl
})
},
[worldName, worldContentServerUrl]
)
const handleConnect = useCallback(() => track(Events.CONNECT), [track])
const handleDisconnect = useCallback(() => {
// This is to avoid tracking the disconnect event twice
if (!alreadyDisconnected) {
track(Events.DISCONNECT)
setAlreadyDisconnected(true)
}
}, [track, alreadyDisconnected])
useEffect(() => {
window.onbeforeunload = () => {
handleDisconnect()
}
return () => {
window.onbeforeunload = null
}
}, [])
return (
<>
<LiveKitRoom token={token} serverUrl={server} connect={true} data-lk-theme="default">
<LiveKitRoom
token={token}
serverUrl={server}
connect={true}
data-lk-theme="default"
onConnected={handleConnect}
onDisconnected={handleDisconnect}
>
<VideoConference />
</LiveKitRoom>
</>

View File

@ -1,19 +1,13 @@
import { Dispatch } from 'redux'
import { RouterProps } from '../../../utils/WithRouter'
export type Props = {
loggedInAddress?: string
profileAddress?: string
isLoading: boolean
server?: string
token?: string
worldContentServerUrl: string
worldName: string
}
export type MapStateProps = Pick<Props, 'loggedInAddress' | 'isLoading' | 'profileAddress' | 'server' | 'token'>
export type MapStateProps = Pick<Props, 'loggedInAddress' | 'isLoading' | 'server' | 'token' | 'worldName' | 'worldContentServerUrl'>
export type MapDispatch = Dispatch
type Params = {
profileAddress?: string
}
export type OwnProps = {
router: RouterProps<Params>
}

View File

@ -1,6 +1,6 @@
import { connect } from 'react-redux'
import { getAddress, isConnecting } from 'decentraland-dapps/dist/modules/wallet/selectors'
import { setServer, setToken } from '../../../modules/conference/action'
import { setServer, setToken, setWorldRelatedInformation } from '../../../modules/conference/action'
import { config } from '../../../modules/config'
import { getCurrentIdentity, isLoggingIn } from '../../../modules/identity/selector'
import { RootState } from '../../../modules/reducer'
@ -22,9 +22,10 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): MapStateProps =>
}
const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({
onSubmitConnectForm: (server: string, token: string) => {
onSubmitConnectForm: (server: string, token: string, worldsContentServerUrl: string, selectedServer: string) => {
dispatch(setServer({ server }))
dispatch(setToken({ token }))
dispatch(setWorldRelatedInformation({ contentServerUrl: worldsContentServerUrl, name: selectedServer }))
}
})

View File

@ -90,7 +90,7 @@ function ConnectToWorld(props: Props) {
if (!identity) return
const response: { url: string; token: string } = await livekitConnect(identity, worldsContentServerUrl, selectedServer)
onSubmitConnectForm(response.url, response.token)
onSubmitConnectForm(response.url, response.token, worldsContentServerUrl, selectedServer)
addServerToPreviouslyLoaded(selectedServer)
navigate(`/meet/${encodeURIComponent(response.url)}?token=${encodeURIComponent(response.token)}`)
} catch (error) {

View File

@ -8,7 +8,7 @@ export type Props = {
previouslyLoadedServers: string[] | null
identity: AuthIdentity | null
worldsContentServerUrl: string
onSubmitConnectForm: (server: string, token: string) => void
onSubmitConnectForm: (server: string, token: string, worldsContentServerUrl: string, selectedServer: string) => void
}
export type MapStateProps = Pick<Props, 'loggedInAddress' | 'isLoading' | 'previouslyLoadedServers' | 'identity' | 'worldsContentServerUrl'>

View File

@ -0,0 +1,4 @@
export enum Events {
CONNECT = 'Connect',
DISCONNECT = 'Disconnect'
}

View File

@ -1,9 +1,10 @@
import { createAction } from '@reduxjs/toolkit'
export const setServer = createAction<{ server: string }>('Set Server')
export type SetServerAction = ReturnType<typeof setServer>
export const setToken = createAction<{ token: string }>('Set Token')
export type SetTokenAction = ReturnType<typeof setToken>
export const setWorldRelatedInformation = createAction<{ contentServerUrl: string; name: string }>('Set World Related Information')
export type SetWorldRelatedInformationAction = ReturnType<typeof setWorldRelatedInformation>

View File

@ -1,14 +1,22 @@
import { createReducer } from '@reduxjs/toolkit'
import { setServer, setToken } from './action'
import { setServer, setToken, setWorldRelatedInformation } from './action'
export type ConferenceState = {
token: string
server: string
worlds: {
contentServerUrl: string
name: string
}
}
export const INITIAL_STATE: ConferenceState = {
token: '',
server: ''
server: '',
worlds: {
contentServerUrl: '',
name: ''
}
}
export const conferenceReducer = createReducer<ConferenceState>(INITIAL_STATE, builder =>
@ -19,4 +27,12 @@ export const conferenceReducer = createReducer<ConferenceState>(INITIAL_STATE, b
.addCase(setToken, (state, action) => {
state.token = action.payload.token
})
.addCase(setWorldRelatedInformation, (state, action) => {
const { contentServerUrl, name } = action.payload
state.worlds = {
contentServerUrl,
name
}
})
)

View File

@ -6,4 +6,8 @@ const getState = (state: RootState) => state.conference
export const getToken = (state: RootState) => getState(state).token
export const getServer = (state: RootState) => getState(state).server
const getWorlds = (state: RootState) => getState(state).worlds
export const getWorldName = (state: RootState) => getWorlds(state).name
export const getWorldContentServerUrl = (state: RootState) => getWorlds(state).contentServerUrl
export const isLoading = createSelector([getToken], token => !!token)

View File

@ -1,5 +1,6 @@
{
"CHAIN_ID": "11155111",
"PEER_URL": "https://peer.decentraland.zone",
"WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.zone"
"WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.zone",
"SEGMENT_API_KEY": "XiymjAbQV5OJZXaT1doKqHgvgoUGHprP"
}

View File

@ -2,5 +2,6 @@
"CHAIN_ID": "1",
"EXPLORER_URL": "https://play.decentraland.org",
"PEER_URL": "https://peer.decentraland.org",
"WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.org"
"WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.org",
"SEGMENT_API_KEY": "FLpPrzJ8NlZLoRDldyQz4VdRnFGtxCAb"
}

View File

@ -1,5 +1,6 @@
{
"CHAIN_ID": "1",
"PEER_URL": "https://peer.decentraland.org",
"WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.org"
"WORLDS_CONTENT_SERVER_URL": "https://worlds-content-server.decentraland.org",
"SEGMENT_API_KEY": "FLpPrzJ8NlZLoRDldyQz4VdRnFGtxCAb"
}

View File

@ -21,12 +21,6 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src",
".eslintrc.js",
"vite.config.ts",
"jest.config.ts",
"scripts"
],
"include": ["src", ".eslintrc.js", "vite.config.ts", "jest.config.ts", "scripts"],
"references": [{ "path": "./tsconfig.node.json" }]
}