From 971eca59ff9071fe8726fa1855283cf47e162a45 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Mar 2023 18:40:16 -0400 Subject: [PATCH 01/43] Opt into analytics by default during the beta --- public/locales/en-GB/app.json | 4 ++-- src/analytics/AnalyticsNotice.tsx | 14 ++++++++++++++ src/analytics/AnalyticsOptInDescription.tsx | 20 -------------------- src/form/Form.tsx | 4 ++-- src/home/RegisteredView.module.css | 4 ++++ src/home/RegisteredView.tsx | 20 ++++++++------------ src/home/UnauthenticatedView.module.css | 4 ++++ src/home/UnauthenticatedView.tsx | 20 ++++++++------------ src/room/RoomPage.tsx | 7 +++++++ src/settings/SettingsModal.module.css | 4 ++-- src/settings/SettingsModal.tsx | 19 +++++++++++++++---- src/settings/useSetting.ts | 8 +++++++- src/tabs/Tabs.module.css | 4 +++- 13 files changed, 76 insertions(+), 56 deletions(-) create mode 100644 src/analytics/AnalyticsNotice.tsx delete mode 100644 src/analytics/AnalyticsOptInDescription.tsx diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index fe52c85..389eeb7 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -8,6 +8,7 @@ "{{name}} is talking…": "{{name}} is talking…", "{{names}}, {{name}}": "{{names}}, {{name}}", "{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie call", + "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.": "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "<0>Already have an account?<1><0>Log in Or <2>Access as a guest": "<0>Already have an account?<1><0>Log in Or <2>Access as a guest", "<0>Create an account Or <2>Access as a guest": "<0>Create an account Or <2>Access as a guest", "<0>Join call now<1>Or<2>Copy call link and join later": "<0>Join call now<1>Or<2>Copy call link and join later", @@ -21,7 +22,7 @@ "Avatar": "Avatar", "By clicking \"Go\", you agree to our <2>Terms and conditions": "By clicking \"Go\", you agree to our <2>Terms and conditions", "By clicking \"Join call now\", you agree to our <2>Terms and conditions": "By clicking \"Join call now\", you agree to our <2>Terms and conditions", - "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ": "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our ", + "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.": "By participating in this beta, you consent to the collection of anonymous data, which we use to improve the product. You can find more information about which data we track in our <2>Privacy Policy and our <5>Cookie Policy.", "Call link copied": "Call link copied", "Call type menu": "Call type menu", "Camera": "Camera", @@ -85,7 +86,6 @@ "Press and hold spacebar to talk over {{name}}": "Press and hold spacebar to talk over {{name}}", "Press and hold to talk": "Press and hold to talk", "Press and hold to talk over {{name}}": "Press and hold to talk over {{name}}", - "Privacy Policy": "Privacy Policy", "Profile": "Profile", "Recaptcha dismissed": "Recaptcha dismissed", "Recaptcha not loaded": "Recaptcha not loaded", diff --git a/src/analytics/AnalyticsNotice.tsx b/src/analytics/AnalyticsNotice.tsx new file mode 100644 index 0000000..feceef7 --- /dev/null +++ b/src/analytics/AnalyticsNotice.tsx @@ -0,0 +1,14 @@ +import React, { FC } from "react"; +import { Trans } from "react-i18next"; + +import { Link } from "../typography/Typography"; + +export const AnalyticsNotice: FC = () => ( + + By participating in this beta, you consent to the collection of anonymous + data, which we use to improve the product. You can find more information + about which data we track in our{" "} + Privacy Policy and our{" "} + Cookie Policy. + +); diff --git a/src/analytics/AnalyticsOptInDescription.tsx b/src/analytics/AnalyticsOptInDescription.tsx deleted file mode 100644 index 46727f5..0000000 --- a/src/analytics/AnalyticsOptInDescription.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { t } from "i18next"; -import React from "react"; - -import { Link } from "../typography/Typography"; - -export const optInDescription: () => JSX.Element = () => { - return ( - <> - <> - {t( - "By ticking this box you consent to the collection of anonymous data, which we use to improve your experience. You can find more information about which data we track in our " - )} - - - <>{t("Privacy Policy")} - - . - - ); -}; diff --git a/src/form/Form.tsx b/src/form/Form.tsx index beea4c8..fd98a62 100644 --- a/src/form/Form.tsx +++ b/src/form/Form.tsx @@ -15,14 +15,14 @@ limitations under the License. */ import classNames from "classnames"; -import React, { FormEventHandler, forwardRef } from "react"; +import React, { FormEventHandler, forwardRef, ReactNode } from "react"; import styles from "./Form.module.css"; interface FormProps { className: string; onSubmit: FormEventHandler; - children: JSX.Element[]; + children: ReactNode[]; } export const Form = forwardRef( diff --git a/src/home/RegisteredView.module.css b/src/home/RegisteredView.module.css index ae43b5b..a96bd53 100644 --- a/src/home/RegisteredView.module.css +++ b/src/home/RegisteredView.module.css @@ -37,3 +37,7 @@ limitations under the License. .recentCallsTitle { margin-bottom: 32px; } + +.notice { + color: var(--secondary-content); +} diff --git a/src/home/RegisteredView.tsx b/src/home/RegisteredView.tsx index a6278c2..06a6720 100644 --- a/src/home/RegisteredView.tsx +++ b/src/home/RegisteredView.tsx @@ -39,11 +39,11 @@ import { CallList } from "./CallList"; import { UserMenuContainer } from "../UserMenuContainer"; import { useModalTriggerState } from "../Modal"; import { JoinExistingCallModal } from "./JoinExistingCallModal"; -import { Title } from "../typography/Typography"; +import { Caption, Title } from "../typography/Typography"; import { Form } from "../form/Form"; import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; import { useOptInAnalytics } from "../settings/useSetting"; -import { optInDescription } from "../analytics/AnalyticsOptInDescription"; +import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; interface Props { client: MatrixClient; @@ -54,7 +54,7 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { const [callType, setCallType] = useState(CallType.Video); const [loading, setLoading] = useState(false); const [error, setError] = useState(); - const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); + const [optInAnalytics] = useOptInAnalytics(); const history = useHistory(); const { t } = useTranslation(); const { modalState, modalProps } = useModalTriggerState(); @@ -144,15 +144,11 @@ export function RegisteredView({ client, isPasswordlessUser }: Props) { {loading ? t("Loading…") : t("Go")} - ) => - setOptInAnalytics(event.target.checked) - } - /> + {optInAnalytics === null && ( + + + + )} {error && ( diff --git a/src/home/UnauthenticatedView.module.css b/src/home/UnauthenticatedView.module.css index bad173e..49c272b 100644 --- a/src/home/UnauthenticatedView.module.css +++ b/src/home/UnauthenticatedView.module.css @@ -45,3 +45,7 @@ limitations under the License. display: none; } } + +.notice { + color: var(--secondary-content); +} diff --git a/src/home/UnauthenticatedView.tsx b/src/home/UnauthenticatedView.tsx index 6339e42..c31637b 100644 --- a/src/home/UnauthenticatedView.tsx +++ b/src/home/UnauthenticatedView.tsx @@ -39,15 +39,15 @@ import { CallType, CallTypeDropdown } from "./CallTypeDropdown"; import styles from "./UnauthenticatedView.module.css"; import commonStyles from "./common.module.css"; import { generateRandomName } from "../auth/generateRandomName"; +import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; import { useOptInAnalytics } from "../settings/useSetting"; -import { optInDescription } from "../analytics/AnalyticsOptInDescription"; export const UnauthenticatedView: FC = () => { const { setClient } = useClient(); const [callType, setCallType] = useState(CallType.Video); const [loading, setLoading] = useState(false); const [error, setError] = useState(); - const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); + const [optInAnalytics] = useOptInAnalytics(); const [privacyPolicyUrl, recaptchaKey, register] = useInteractiveRegistration(); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); @@ -155,16 +155,12 @@ export const UnauthenticatedView: FC = () => { autoComplete="off" /> - ) => - setOptInAnalytics(event.target.checked) - } - /> - + {optInAnalytics === null && ( + + + + )} + By clicking "Go", you agree to our{" "} Terms and conditions diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index f4e460d..fe91861 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -27,6 +27,7 @@ import { useUrlParams } from "../UrlParams"; import { MediaHandlerProvider } from "../settings/useMediaHandler"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { translatedError } from "../TranslatedError"; +import { useOptInAnalytics } from "../settings/useSetting"; export const RoomPage: FC = () => { const { t } = useTranslation(); @@ -46,9 +47,15 @@ export const RoomPage: FC = () => { const roomIdOrAlias = roomId ?? roomAlias; if (!roomIdOrAlias) throw translatedError("No room specified", t); + const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics(); const { registerPasswordlessUser } = useRegisterPasswordlessUser(); const [isRegistering, setIsRegistering] = useState(false); + useEffect(() => { + // During the beta, opt into analytics by default + if (optInAnalytics === null) setOptInAnalytics(true); + }, [optInAnalytics, setOptInAnalytics]); + useEffect(() => { // If we've finished loading, are not already authed and we've been given a display name as // a URL param, automatically register a passwordless user diff --git a/src/settings/SettingsModal.module.css b/src/settings/SettingsModal.module.css index 9b4951b..1e44dad 100644 --- a/src/settings/SettingsModal.module.css +++ b/src/settings/SettingsModal.module.css @@ -20,7 +20,7 @@ limitations under the License. } .tabContainer { - margin: 27px 16px; + padding: 27px 20px; } .fieldRowText { @@ -33,5 +33,5 @@ The "Developer" item in the tab bar can be toggled. Without a defined width activating the developer tab makes the tab container jump to the right. */ .tabLabel { - width: 80px; + min-width: 80px; } diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 90a1cb5..2880831 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from "react"; import { Item } from "@react-stately/collections"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; @@ -39,8 +39,8 @@ import { import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; import { useDownloadDebugLog } from "./submit-rageshake"; -import { Body } from "../typography/Typography"; -import { optInDescription } from "../analytics/AnalyticsOptInDescription"; +import { Body, Caption } from "../typography/Typography"; +import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; interface Props { isOpen: boolean; @@ -71,6 +71,17 @@ export const SettingsModal = (props: Props) => { const downloadDebugLog = useDownloadDebugLog(); + const optInDescription = ( + + + +
+ You may withdraw consent by unchecking this box. If you are currently in + a call, this setting will take effect at the end of the call. +
+ + ); + return ( { id="optInAnalytics" type="checkbox" checked={optInAnalytics} - description={optInDescription()} + description={optInDescription} onChange={(event: React.ChangeEvent) => setOptInAnalytics(event.target.checked) } diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 756ac74..0fe5fe2 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -87,9 +87,15 @@ export const useSpatialAudio = (): [boolean, (val: boolean) => void] => { }; export const useShowInspector = () => useSetting("show-inspector", false); -export const useOptInAnalytics = () => useSetting("opt-in-analytics", false); + +// null = undecided +export const useOptInAnalytics = () => + useSetting("opt-in-analytics", null); + export const useKeyboardShortcuts = () => useSetting("keyboard-shortcuts", true); + export const useNewGrid = () => useSetting("new-grid", false); + export const useDeveloperSettingsTab = () => useSetting("developer-settings-tab", false); diff --git a/src/tabs/Tabs.module.css b/src/tabs/Tabs.module.css index bad7a0e..188747c 100644 --- a/src/tabs/Tabs.module.css +++ b/src/tabs/Tabs.module.css @@ -88,7 +88,9 @@ limitations under the License. .tabContainer { width: 100%; flex-direction: row; - margin: 27px 16px; + padding: 27px 20px; + box-sizing: border-box; + overflow: hidden; } .tabList { From 1bf1813a77f284b832c402818e279c89b6049f3e Mon Sep 17 00:00:00 2001 From: alariej Date: Fri, 17 Mar 2023 17:20:16 +0100 Subject: [PATCH 02/43] Fix for Android WebView, which does not support navigator.mediaSession --- src/room/useGroupCall.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 60f2d67..d6c785a 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -189,7 +189,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { ]; for (const mediaAction of mediaActions) { - navigator.mediaSession.setActionHandler( + navigator.mediaSession?.setActionHandler( mediaAction, doNothingMediaActionCallback ); @@ -197,7 +197,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { return () => { for (const mediaAction of mediaActions) { - navigator.mediaSession.setActionHandler(mediaAction, null); + navigator.mediaSession?.setActionHandler(mediaAction, null); } }; }, [doNothingMediaActionCallback]); From 698bea93e3eaefb788012d4ce4f51c062e1f4675 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 22 Mar 2023 11:33:50 -0400 Subject: [PATCH 03/43] Update matrix-widget-api --- package.json | 2 +- yarn.lock | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b6368d..f7e2263 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8cbbdaa239e449848e8874f041ef1879c1956696", - "matrix-widget-api": "^1.0.0", + "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", "pako": "^2.0.4", diff --git a/yarn.lock b/yarn.lock index 3b6c84f..cd1d7ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10387,6 +10387,14 @@ matrix-widget-api@^1.0.0: "@types/events" "^3.0.0" events "^3.2.0" +matrix-widget-api@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" + integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== + dependencies: + "@types/events" "^3.0.0" + events "^3.2.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" From 76c02773017791fb7ca931f89d7150620a3628f5 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Wed, 22 Mar 2023 11:41:41 -0400 Subject: [PATCH 04/43] Update matrix-js-sdk --- package.json | 2 +- src/room/GroupCallInspector.tsx | 6 +++--- yarn.lock | 26 +++++++++----------------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index f7e2263..8a95ba7 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#8cbbdaa239e449848e8874f041ef1879c1956696", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f795577e14d5e56b2f57d4b9a686d832c5210e3d", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 648a0a1..399f8d7 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -31,7 +31,7 @@ import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; +import { CallEvent, VoipEvent } from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; @@ -235,7 +235,7 @@ function reducer( action: { type?: CallEvent | ClientEvent | RoomStateEvent; event?: MatrixEvent; - rawEvent?: Record; + rawEvent?: VoipEvent; callStateEvent?: MatrixEvent; memberStateEvents?: MatrixEvent[]; } @@ -387,7 +387,7 @@ function useGroupCallState( dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); } - function onSendVoipEvent(event: Record) { + function onSendVoipEvent(event: VoipEvent) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); } diff --git a/yarn.lock b/yarn.lock index cd1d7ef..1852bfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,10 +1821,10 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": - version "0.1.0-alpha.4" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.4.tgz#1b20294e0354c3dcc9c7dc810d883198a4042f04" - integrity sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.5": + version "0.1.0-alpha.5" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" + integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -10362,31 +10362,23 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8cbbdaa239e449848e8874f041ef1879c1956696": - version "23.4.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8cbbdaa239e449848e8874f041ef1879c1956696" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#f795577e14d5e56b2f57d4b9a686d832c5210e3d": + version "23.5.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f795577e14d5e56b2f57d4b9a686d832c5210e3d" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.0.0" + matrix-widget-api "^1.3.1" p-retry "4" sdp-transform "^2.14.1" unhomoglyph "^1.0.6" uuid "9" -matrix-widget-api@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.1.1.tgz#d3fec45033d0cbc14387a38ba92dac4dbb1be962" - integrity sha512-gNSgmgSwvOsOcWK9k2+tOhEMYBiIMwX95vMZu0JqY7apkM02xrOzUBuPRProzN8CnbIALH7e3GAhatF6QCNvtA== - dependencies: - "@types/events" "^3.0.0" - events "^3.2.0" - matrix-widget-api@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" From d1ba5dff38f974574307778829f5ce66fa00a2c1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Mar 2023 14:37:25 +0000 Subject: [PATCH 05/43] Allow all origins --- config/otel_dev/collector-gateway.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 6cc85a0..52f90c3 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -5,7 +5,7 @@ receivers: endpoint: 0.0.0.0:4318 cors: allowed_origins: - - "http://*" + - "*" allowed_headers: - "*" processors: From 5f41f9476bc1827664974c2207b7da664a0d8620 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 23 Mar 2023 13:07:34 -0400 Subject: [PATCH 06/43] Disable the opt in analytics setting if Posthog isn't configured --- src/settings/SettingsModal.tsx | 11 +++++------ src/settings/useSetting.ts | 23 ++++++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 2880831..6e38b5f 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -32,7 +32,6 @@ import { useSpatialAudio, useShowInspector, useOptInAnalytics, - canEnableSpatialAudio, useNewGrid, useDeveloperSettingsTab, } from "./useSetting"; @@ -133,16 +132,16 @@ export const SettingsModal = (props: Props) => { label={t("Spatial audio")} type="checkbox" checked={spatialAudio} - disabled={!canEnableSpatialAudio()} + disabled={setSpatialAudio === null} description={ - canEnableSpatialAudio() - ? t( + setSpatialAudio === null + ? t("This feature is only supported on Firefox.") + : t( "This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)" ) - : t("This feature is only supported on Firefox.") } onChange={(event: React.ChangeEvent) => - setSpatialAudio(event.target.checked) + setSpatialAudio!(event.target.checked) } /> diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 0fe5fe2..13288b6 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -16,6 +16,10 @@ limitations under the License. import { EventEmitter } from "events"; import { useMemo, useState, useEffect, useCallback } from "react"; +import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; + +type Setting = [T, (value: T) => void]; +type DisableableSetting = [T, ((value: T) => void) | null]; // Bus to notify other useSetting consumers when a setting is changed export const settingsBus = new EventEmitter(); @@ -24,10 +28,7 @@ const getSettingKey = (name: string): string => { return `matrix-setting-${name}`; }; // Like useState, but reads from and persists the value to localStorage -const useSetting = ( - name: string, - defaultValue: T -): [T, (value: T) => void] => { +const useSetting = (name: string, defaultValue: T): Setting => { const key = useMemo(() => getSettingKey(name), [name]); const [value, setValue] = useState(() => { @@ -65,7 +66,7 @@ export const setSetting = (name: string, newValue: T) => { settingsBus.emit(name, newValue); }; -export const canEnableSpatialAudio = () => { +const canEnableSpatialAudio = () => { const { userAgent } = navigator; // Spatial audio means routing audio through audio contexts. On Chrome, // this bypasses the AEC processor and so breaks echo cancellation. @@ -79,18 +80,22 @@ export const canEnableSpatialAudio = () => { return userAgent.includes("Firefox"); }; -export const useSpatialAudio = (): [boolean, (val: boolean) => void] => { +export const useSpatialAudio = (): DisableableSetting => { const settingVal = useSetting("spatial-audio", false); if (canEnableSpatialAudio()) return settingVal; - return [false, (_: boolean) => {}]; + return [false, null]; }; export const useShowInspector = () => useSetting("show-inspector", false); // null = undecided -export const useOptInAnalytics = () => - useSetting("opt-in-analytics", null); +export const useOptInAnalytics = (): DisableableSetting => { + const settingVal = useSetting("opt-in-analytics", null); + if (PosthogAnalytics.instance.isEnabled()) return settingVal; + + return [false, null]; +}; export const useKeyboardShortcuts = () => useSetting("keyboard-shortcuts", true); From 40f5c53c052f77daf23e72810e13a63f952af356 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 24 Mar 2023 09:31:52 +0000 Subject: [PATCH 07/43] Put CORS header back to http://* with comment on why browsers are annoying --- config/otel_dev/collector-gateway.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 52f90c3..7b1fad0 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -5,7 +5,9 @@ receivers: endpoint: 0.0.0.0:4318 cors: allowed_origins: - - "*" + # This can't be '*' because opentelemetry-js uses sendBeacon which always operates + # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' + - "http://*" allowed_headers: - "*" processors: From c4f029ae4f831d389d74139adc30d84bece6ddba Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 27 Mar 2023 22:30:12 -0400 Subject: [PATCH 08/43] Fix lint error --- src/settings/useSetting.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/settings/useSetting.ts b/src/settings/useSetting.ts index 13288b6..f2a2bfc 100644 --- a/src/settings/useSetting.ts +++ b/src/settings/useSetting.ts @@ -16,6 +16,7 @@ limitations under the License. import { EventEmitter } from "events"; import { useMemo, useState, useEffect, useCallback } from "react"; + import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; type Setting = [T, (value: T) => void]; From 66c3d05ae936d7dcd56607184cbf2885619f317c Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Tue, 28 Mar 2023 11:51:15 +0200 Subject: [PATCH 09/43] docu: Add webrtc metric to OTel --- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 92 +++++++++++- src/otel/ObjectFlattener.ts | 83 +++++++++++ src/room/useGroupCall.ts | 36 +++++ test/otel/ObjectFlattene-test.ts | 215 ++++++++++++++++++++++++++++ 5 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 src/otel/ObjectFlattener.ts create mode 100644 test/otel/ObjectFlattene-test.ts diff --git a/package.json b/package.json index a32fd8c..3f31e74 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#2cd38e91eee1f5b16a9be0caba6ff19486b95f31", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 764249f..c11b6e0 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -23,8 +23,15 @@ import { RoomMember, } from "matrix-js-sdk"; import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; +import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; +import { + ConnectionStatsReport, + ByteSentStatsReport, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; +import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils"; import { ElementCallOpenTelemetry } from "./otel"; +import { ObjectFlattener } from "./ObjectFlattener"; /** * Flattens out an object into a single layer with components @@ -73,12 +80,24 @@ function flattenVoipEventRecursive( */ export class OTelGroupCallMembership { private callMembershipSpan?: Span; - private myUserId: string; - private myMember: RoomMember; + private myUserId = "unknown"; + private myMember?: RoomMember; + private statsReportSpan: { + span: Span | undefined; + stats: OTelStatsReportEvent[]; + }; constructor(private groupCall: GroupCall, client: MatrixClient) { - this.myUserId = client.getUserId(); - this.myMember = groupCall.room.getMember(client.getUserId()); + const clientId = client.getUserId(); + if (clientId) { + this.myUserId = clientId; + const myMember = groupCall.room.getMember(clientId); + if (myMember) { + this.myMember = myMember; + } + } + + this.statsReportSpan = { span: undefined, stats: [] }; ElementCallOpenTelemetry.instance.provider.resource.attributes[ SemanticResourceAttributes.SERVICE_NAME @@ -98,7 +117,7 @@ export class OTelGroupCallMembership { this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); this.callMembershipSpan.setAttribute( "matrix.displayName", - this.myMember.name + this.myMember ? this.myMember.name : "unknown-name" ); opentelemetry.trace.setSpan( @@ -113,7 +132,7 @@ export class OTelGroupCallMembership { this.callMembershipSpan?.addEvent("matrix.leaveCall"); // and end the main span to indicate we've left - if (this.callMembershipSpan) this.callMembershipSpan.end(); + this.callMembershipSpan?.end(); } public onUpdateRoomState(event: MatrixEvent) { @@ -177,4 +196,65 @@ export class OTelGroupCallMembership { "matrix.screensharing.enabled": newValue, }); } + + public onConnectionStatsReport( + statsReport: GroupCallStatsReport + ) { + const type = OTelStatsReportType.ConnectionStatsReport; + const data = + ObjectFlattener.flattenConnectionStatsReportObject(statsReport); + this.buildStatsEventSpan({ type, data }); + } + + public onByteSentStatsReport( + statsReport: GroupCallStatsReport + ) { + const type = OTelStatsReportType.ByteSentStatsReport; + const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport); + this.buildStatsEventSpan({ type, data }); + } + + private buildStatsEventSpan(event: OTelStatsReportEvent): void { + if (this.statsReportSpan.span === undefined && this.callMembershipSpan) { + const ctx = setSpan( + opentelemetry.context.active(), + this.callMembershipSpan + ); + this.statsReportSpan.span = + ElementCallOpenTelemetry.instance.tracer.startSpan( + "matrix.groupCallMembership.statsReport", + undefined, + ctx + ); + this.statsReportSpan.span.setAttribute( + "matrix.confId", + this.groupCall.groupCallId + ); + this.statsReportSpan.span.setAttribute("matrix.userId", this.myUserId); + this.statsReportSpan.span.setAttribute( + "matrix.displayName", + this.myMember ? this.myMember.name : "unknown-name" + ); + + this.statsReportSpan.span.addEvent(event.type, event.data); + this.statsReportSpan.stats.push(event); + } else if ( + this.statsReportSpan.span !== undefined && + this.callMembershipSpan + ) { + this.statsReportSpan.span.addEvent(event.type, event.data); + this.statsReportSpan.span.end(); + this.statsReportSpan = { span: undefined, stats: [] }; + } + } +} + +interface OTelStatsReportEvent { + type: OTelStatsReportType; + data: Attributes; +} + +enum OTelStatsReportType { + ConnectionStatsReport = "matrix.stats.connection", + ByteSentStatsReport = "matrix.stats.byteSent", } diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts new file mode 100644 index 0000000..d45360c --- /dev/null +++ b/src/otel/ObjectFlattener.ts @@ -0,0 +1,83 @@ +/* +Copyright 2023 New Vector Ltd + +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 { Attributes } from "@opentelemetry/api"; +import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; +import { + ByteSentStatsReport, + ConnectionStatsReport, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; + +export class ObjectFlattener { + public static flattenConnectionStatsReportObject( + statsReport: GroupCallStatsReport + ): Attributes { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report, + flatObject, + "matrix.stats.conn.", + 0 + ); + return flatObject; + } + + public static flattenByteSentStatsReportObject( + statsReport: GroupCallStatsReport + ): Attributes { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report, + flatObject, + "matrix.stats.bytesSent.", + 0 + ); + return flatObject; + } + + public static flattenObjectRecursive( + obj: Object, + flatObject: Attributes, + prefix: string, + depth: number + ): void { + if (depth > 10) + throw new Error( + "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + + prefix + ); + let entries; + if (obj instanceof Map) { + entries = obj.entries(); + } else { + entries = Object.entries(obj); + } + for (const [k, v] of entries) { + if (["string", "number", "boolean"].includes(typeof v) || v === null) { + let value; + value = v === null ? "null" : v; + value = typeof v === "number" && Number.isNaN(v) ? "NaN" : value; + flatObject[prefix + k] = value; + } else if (typeof v === "object") { + ObjectFlattener.flattenObjectRecursive( + v, + flatObject, + prefix + k + ".", + depth + 1 + ); + } + } + } +} diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 0b54c82..1c69181 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -22,12 +22,18 @@ import { GroupCallErrorCode, GroupCallUnknownDeviceError, GroupCallError, + GroupCallStatsReportEvent, + GroupCallStatsReport, } from "matrix-js-sdk/src/webrtc/groupCall"; import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useTranslation } from "react-i18next"; import { IWidgetApiRequest } from "matrix-widget-api"; import { MatrixClient } from "matrix-js-sdk"; +import { + ByteSentStatsReport, + ConnectionStatsReport, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; @@ -330,6 +336,18 @@ export function useGroupCall( } } + function onConnectionStatsReport( + report: GroupCallStatsReport + ): void { + groupCallOTelMembership?.onConnectionStatsReport(report); + } + + function onByteSentStatsReport( + report: GroupCallStatsReport + ): void { + groupCallOTelMembership?.onByteSentStatsReport(report); + } + groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged); groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged); groupCall.on( @@ -346,6 +364,16 @@ export function useGroupCall( groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged); groupCall.on(GroupCallEvent.Error, onError); + groupCall.on( + GroupCallStatsReportEvent.ConnectionStats, + onConnectionStatsReport + ); + + groupCall.on( + GroupCallStatsReportEvent.ByteSentStats, + onByteSentStatsReport + ); + updateState({ error: null, state: groupCall.state, @@ -392,6 +420,14 @@ export function useGroupCall( onParticipantsChanged ); groupCall.removeListener(GroupCallEvent.Error, onError); + groupCall.removeListener( + GroupCallStatsReportEvent.ConnectionStats, + onConnectionStatsReport + ); + groupCall.removeListener( + GroupCallStatsReportEvent.ByteSentStats, + onByteSentStatsReport + ); groupCall.leave(); }; }, [groupCall, updateState]); diff --git a/test/otel/ObjectFlattene-test.ts b/test/otel/ObjectFlattene-test.ts new file mode 100644 index 0000000..a0258c0 --- /dev/null +++ b/test/otel/ObjectFlattene-test.ts @@ -0,0 +1,215 @@ +import { ObjectFlattener } from "../../src/otel/ObjectFlattener"; + +/* +Copyright 2023 New Vector Ltd + +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. +*/ +describe("ObjectFlattener", () => { + const statsReport = { + report: { + bandwidth: { upload: 426, download: 0 }, + bitrate: { + upload: 426, + download: 0, + audio: { + upload: 124, + download: 0, + }, + video: { + upload: 302, + download: 0, + }, + }, + packetLoss: { + total: 0, + download: 0, + upload: 0, + }, + framerate: { + local: new Map([ + ["LOCAL_AUDIO_TRACK_ID", 0], + ["LOCAL_VIDEO_TRACK_ID", 30], + ]), + remote: new Map([ + ["REMOTE_AUDIO_TRACK_ID", 0], + ["REMOTE_VIDEO_TRACK_ID", 60], + ]), + }, + resolution: { + local: new Map([ + ["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }], + ["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }], + ]), + remote: new Map([ + ["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }], + ["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }], + ]), + }, + codec: { + local: new Map([ + ["LOCAL_AUDIO_TRACK_ID", "opus"], + ["LOCAL_VIDEO_TRACK_ID", "v8"], + ]), + remote: new Map([ + ["REMOTE_AUDIO_TRACK_ID", "opus"], + ["REMOTE_VIDEO_TRACK_ID", "v9"], + ]), + }, + transport: [ + { + ip: "ff11::5fa:abcd:999c:c5c5:50000", + type: "udp", + localIp: "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", + isFocus: true, + localCandidateType: "host", + remoteCandidateType: "host", + networkType: "ethernet", + rtt: NaN, + }, + { + ip: "10.10.10.2:22222", + type: "tcp", + localIp: "10.10.10.100:33333", + isFocus: true, + localCandidateType: "srfx", + remoteCandidateType: "srfx", + networkType: "ethernet", + rtt: null, + }, + ], + }, + }; + describe("on flattenObjectRecursive", () => { + it("should flatter an Map object", () => { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report.resolution, + flatObject, + "matrix.stats.conn.resolution.", + 0 + ); + expect(flatObject).toEqual({ + "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.height": -1, + "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.width": -1, + + "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, + "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, + + "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": -1, + "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": -1, + + "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, + "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, + }); + }); + it("should flatter an Array object", () => { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report.transport, + flatObject, + "matrix.stats.conn.transport.", + 0 + ); + expect(flatObject).toEqual({ + "matrix.stats.conn.transport.0.ip": "ff11::5fa:abcd:999c:c5c5:50000", + "matrix.stats.conn.transport.0.type": "udp", + "matrix.stats.conn.transport.0.localIp": + "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", + "matrix.stats.conn.transport.0.isFocus": true, + "matrix.stats.conn.transport.0.localCandidateType": "host", + "matrix.stats.conn.transport.0.remoteCandidateType": "host", + "matrix.stats.conn.transport.0.networkType": "ethernet", + "matrix.stats.conn.transport.0.rtt": "NaN", + "matrix.stats.conn.transport.1.ip": "10.10.10.2:22222", + "matrix.stats.conn.transport.1.type": "tcp", + "matrix.stats.conn.transport.1.localIp": "10.10.10.100:33333", + "matrix.stats.conn.transport.1.isFocus": true, + "matrix.stats.conn.transport.1.localCandidateType": "srfx", + "matrix.stats.conn.transport.1.remoteCandidateType": "srfx", + "matrix.stats.conn.transport.1.networkType": "ethernet", + "matrix.stats.conn.transport.1.rtt": "null", + }); + }); + }); + + describe("on flattenConnectionStatsReportObject", () => { + it("should flatten a Report to otel Attributes Object", () => { + expect( + ObjectFlattener.flattenConnectionStatsReportObject(statsReport) + ).toEqual({ + "matrix.stats.conn.bandwidth.download": 0, + "matrix.stats.conn.bandwidth.upload": 426, + "matrix.stats.conn.bitrate.audio.download": 0, + "matrix.stats.conn.bitrate.audio.upload": 124, + "matrix.stats.conn.bitrate.download": 0, + "matrix.stats.conn.bitrate.upload": 426, + "matrix.stats.conn.bitrate.video.download": 0, + "matrix.stats.conn.bitrate.video.upload": 302, + "matrix.stats.conn.codec.local.LOCAL_AUDIO_TRACK_ID": "opus", + "matrix.stats.conn.codec.local.LOCAL_VIDEO_TRACK_ID": "v8", + "matrix.stats.conn.codec.remote.REMOTE_AUDIO_TRACK_ID": "opus", + "matrix.stats.conn.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9", + "matrix.stats.conn.framerate.local.LOCAL_AUDIO_TRACK_ID": 0, + "matrix.stats.conn.framerate.local.LOCAL_VIDEO_TRACK_ID": 30, + "matrix.stats.conn.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0, + "matrix.stats.conn.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60, + "matrix.stats.conn.packetLoss.download": 0, + "matrix.stats.conn.packetLoss.total": 0, + "matrix.stats.conn.packetLoss.upload": 0, + "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.height": -1, + "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.width": -1, + "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, + "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, + "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": -1, + "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": -1, + "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, + "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, + "matrix.stats.conn.transport.0.ip": "ff11::5fa:abcd:999c:c5c5:50000", + "matrix.stats.conn.transport.0.type": "udp", + "matrix.stats.conn.transport.0.localIp": + "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", + "matrix.stats.conn.transport.0.isFocus": true, + "matrix.stats.conn.transport.0.localCandidateType": "host", + "matrix.stats.conn.transport.0.remoteCandidateType": "host", + "matrix.stats.conn.transport.0.networkType": "ethernet", + "matrix.stats.conn.transport.0.rtt": "NaN", + "matrix.stats.conn.transport.1.ip": "10.10.10.2:22222", + "matrix.stats.conn.transport.1.type": "tcp", + "matrix.stats.conn.transport.1.localIp": "10.10.10.100:33333", + "matrix.stats.conn.transport.1.isFocus": true, + "matrix.stats.conn.transport.1.localCandidateType": "srfx", + "matrix.stats.conn.transport.1.remoteCandidateType": "srfx", + "matrix.stats.conn.transport.1.networkType": "ethernet", + "matrix.stats.conn.transport.1.rtt": "null", + }); + }); + }); + + describe("on flattenByteSendStatsReportObject", () => { + const byteSent = { + report: new Map([ + ["4aa92608-04c6-428e-8312-93e17602a959", 132093], + ["a08e4237-ee30-4015-a932-b676aec894b1", 913448], + ]), + }; + it("should flatten a Report to otel Attributes Object", () => { + expect( + ObjectFlattener.flattenByteSentStatsReportObject(byteSent) + ).toEqual({ + "matrix.stats.bytesSent.4aa92608-04c6-428e-8312-93e17602a959": 132093, + "matrix.stats.bytesSent.a08e4237-ee30-4015-a932-b676aec894b1": 913448, + }); + }); + }); +}); From 247d15cbb514eafc87e6d11da0381e61f897f715 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Mar 2023 15:24:33 +0100 Subject: [PATCH 10/43] Update js-sdk for https://github.com/matrix-org/matrix-js-sdk/commit/da03c3b529576a8fcde6f2c9a171fa6cca012830 --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 8a95ba7..efa077d 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f795577e14d5e56b2f57d4b9a686d832c5210e3d", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#da03c3b529576a8fcde6f2c9a171fa6cca012830", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index 1852bfe..539ebf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10362,9 +10362,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#f795577e14d5e56b2f57d4b9a686d832c5210e3d": - version "23.5.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f795577e14d5e56b2f57d4b9a686d832c5210e3d" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#da03c3b529576a8fcde6f2c9a171fa6cca012830": + version "24.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/da03c3b529576a8fcde6f2c9a171fa6cca012830" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" From d53be695f906615b7a49893e1d102aa5199fa5ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Mar 2023 15:33:11 +0100 Subject: [PATCH 11/43] Work around Vite unbounded memory usage Port fix from otel branch where it appears to be working --- .github/workflows/build.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eee1406..ba27e26 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,6 +23,9 @@ jobs: SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_URL: ${{ secrets.SENTRY_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + # This appears to be necessary to stop Vite from OOMing + # https://github.com/vitejs/vite/issues/2433 + NODE_OPTIONS: "--max-old-space-size=16384" - name: Upload Artifact uses: actions/upload-artifact@v2 with: From 15e4c01c5d45d7506fd8ea86bbeae1c4de64e5d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 28 Mar 2023 15:47:59 +0100 Subject: [PATCH 12/43] Add max old space fix to publish workflow too --- .github/workflows/publish.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index f49980d..3558797 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -40,6 +40,9 @@ jobs: SENTRY_URL: ${{ secrets.SENTRY_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} VITE_APP_VERSION: ${{ github.event.release.tag_name }} + # This appears to be necessary to stop Vite from OOMing + # https://github.com/vitejs/vite/issues/2433 + NODE_OPTIONS: "--max-old-space-size=16384" - name: Create Tarball env: From 77c6357b08b2f71a890e000a091536312d4b0648 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2023 12:28:04 +0100 Subject: [PATCH 13/43] Use js-sdk from hangup refactor branch https://github.com/matrix-org/matrix-js-sdk/pull/3234 --- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 86 ++++++++++++++++++++++++++--- src/otel/otel.ts | 4 +- src/room/GroupCallInspector.tsx | 23 ++++++-- src/room/useGroupCall.ts | 2 + yarn.lock | 20 +++++-- 6 files changed, 114 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index a32fd8c..4ecc42d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187", "matrix-widget-api": "^1.0.0", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 764249f..4c6d308 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -14,15 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import opentelemetry, { Span, Attributes } from "@opentelemetry/api"; -import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; +import opentelemetry, { Span, Attributes, Context } from "@opentelemetry/api"; +import { logger } from "@sentry/utils"; import { GroupCall, MatrixClient, MatrixEvent, RoomMember, } from "matrix-js-sdk"; -import { VoipEvent } from "matrix-js-sdk/src/webrtc/call"; +import { + CallState, + MatrixCall, + VoipEvent, +} from "matrix-js-sdk/src/webrtc/call"; +import { + CallsByUserAndDevice, + GroupCallEvent, +} from "matrix-js-sdk/src/webrtc/groupCall"; import { ElementCallOpenTelemetry } from "./otel"; @@ -68,21 +76,37 @@ function flattenVoipEventRecursive( } } +interface CallTrackingInfo { + userId: string; + deviceId: string; + call: MatrixCall; + span: Span; +} + /** * Represent the span of time which we intend to be joined to a group call */ export class OTelGroupCallMembership { private callMembershipSpan?: Span; + private groupCallContext?: Context; private myUserId: string; + private myDeviceId: string; private myMember: RoomMember; + private callsByCallId = new Map(); constructor(private groupCall: GroupCall, client: MatrixClient) { this.myUserId = client.getUserId(); + this.myDeviceId = client.getDeviceId(); this.myMember = groupCall.room.getMember(client.getUserId()); - ElementCallOpenTelemetry.instance.provider.resource.attributes[ - SemanticResourceAttributes.SERVICE_NAME - ] = `element-call-${this.myUserId}-${client.getDeviceId()}`; + this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); + } + + dispose() { + this.groupCall.removeListener( + GroupCallEvent.CallsChanged, + this.onCallsChanged + ); } public onJoinCall() { @@ -96,12 +120,13 @@ export class OTelGroupCallMembership { this.groupCall.groupCallId ); this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId); + this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId); this.callMembershipSpan.setAttribute( "matrix.displayName", this.myMember.name ); - opentelemetry.trace.setSpan( + this.groupCallContext = opentelemetry.trace.setSpan( opentelemetry.context.active(), this.callMembershipSpan ); @@ -126,12 +151,55 @@ export class OTelGroupCallMembership { } this.callMembershipSpan?.addEvent( - `otel_onRoomStateEvent_${event.getType()}`, + `matrix.roomStateEvent_${event.getType()}`, flattenVoipEvent(event.getContent()) ); } - public onSendEvent(event: VoipEvent) { + public onCallsChanged = (calls: CallsByUserAndDevice) => { + for (const [userId, userCalls] of calls.entries()) { + for (const [deviceId, call] of userCalls.entries()) { + if (!this.callsByCallId.has(call.callId)) { + const span = ElementCallOpenTelemetry.instance.tracer.startSpan( + `matrix.call`, + undefined, + this.groupCallContext + ); + // XXX: anonymity + span.setAttribute("matrix.call.target.userId", userId); + span.setAttribute("matrix.call.target.deviceId", deviceId); + this.callsByCallId.set(call.callId, { + userId, + deviceId, + call, + span, + }); + } + } + } + + for (const callTrackingInfo of this.callsByCallId.values()) { + const userCalls = calls.get(callTrackingInfo.userId); + if (!userCalls || !userCalls.has(callTrackingInfo.deviceId)) { + callTrackingInfo.span.end(); + this.callsByCallId.delete(callTrackingInfo.call.callId); + } + } + }; + + public onCallStateChange(call: MatrixCall, newState: CallState) { + const callTrackingInfo = this.callsByCallId.get(call.callId); + if (!callTrackingInfo) { + logger.error(`Got call state change for unknown call ID ${call.callId}`); + return; + } + + callTrackingInfo.span.addEvent("matrix.call.stateChange", { + state: newState, + }); + } + + public onSendEvent(call: MatrixCall, event: VoipEvent) { const eventType = event.eventType as string; if (!eventType.startsWith("m.call")) return; diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 25de3ac..5d3e7d3 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -30,7 +30,7 @@ import { Anonymity } from "../analytics/PosthogAnalytics"; import { Config } from "../config/Config"; import { getSetting, settingsBus } from "../settings/useSetting"; -const SERVICE_NAME_BASE = "element-call"; +const SERVICE_NAME = "element-call"; let sharedInstance: ElementCallOpenTelemetry; @@ -58,7 +58,7 @@ export class ElementCallOpenTelemetry { // This is how we can make Jaeger show a reaonsable service in the dropdown on the left. const providerConfig = { resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: `${SERVICE_NAME_BASE}-unauthenticated`, + [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME, }), }; this._provider = new WebTracerProvider(providerConfig); diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index c058297..ad838fb 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -31,7 +31,12 @@ import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event"; import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { CallEvent, VoipEvent } from "matrix-js-sdk/src/webrtc/call"; +import { + CallEvent, + CallState, + MatrixCall, + VoipEvent, +} from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; @@ -390,10 +395,18 @@ function useGroupCallState( dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); } - function onSendVoipEvent(event: VoipEvent) { + function onSendVoipEvent(event: VoipEvent, call: MatrixCall) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); - otelGroupCallMembership?.onSendEvent(event); + otelGroupCallMembership?.onSendEvent(call, event); + } + + function onCallStateChange( + newState: CallState, + _: CallState, + call: MatrixCall + ) { + otelGroupCallMembership?.onCallStateChange(call, newState); } function onUndecryptableToDevice(event: MatrixEvent) { @@ -406,8 +419,8 @@ function useGroupCallState( } client.on(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.on("calls_changed", onCallsChanged); groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); + groupCall.on(CallEvent.State, onCallStateChange); //client.on("state", onCallsChanged); //client.on("hangup", onCallHangup); client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); @@ -417,8 +430,8 @@ function useGroupCallState( return () => { client.removeListener(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.removeListener("calls_changed", onCallsChanged); groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent); + groupCall.removeListener(CallEvent.State, onCallStateChange); //client.removeListener("state", onCallsChanged); //client.removeListener("hangup", onCallHangup); client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 0b54c82..553d418 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -173,6 +173,8 @@ export function useGroupCall( }); if (groupCallOTelMembershipGroupCallId !== groupCall.groupCallId) { + if (groupCallOTelMembership) groupCallOTelMembership.dispose(); + // If the user disables analytics, this will stay around until they leave the call // so analytics will be disabled once they leave. if (ElementCallOpenTelemetry.instance) { diff --git a/yarn.lock b/yarn.lock index ddb3fc6..58b450a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,7 +1821,7 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.5": version "0.1.0-alpha.5" resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== @@ -10545,18 +10545,18 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#23837266fca5ee799b51a722f7b8eefb2f5ac140": - version "23.5.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/23837266fca5ee799b51a722f7b8eefb2f5ac140" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187": + version "24.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/042f2ed76c501c10dde98a31732fd92d862e2187" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.0.0" + matrix-widget-api "^1.3.1" p-retry "4" sdp-transform "^2.14.1" unhomoglyph "^1.0.6" @@ -10570,6 +10570,14 @@ matrix-widget-api@^1.0.0: "@types/events" "^3.0.0" events "^3.2.0" +matrix-widget-api@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" + integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== + dependencies: + "@types/events" "^3.0.0" + events "^3.2.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" From 4bf1fbfd8ef4f79b56c235093f089ed74805c0b7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2023 13:31:47 +0100 Subject: [PATCH 14/43] Gah, the sentry logger --- src/otel/OTelGroupCallMembership.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 4c6d308..432970b 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -15,13 +15,13 @@ limitations under the License. */ import opentelemetry, { Span, Attributes, Context } from "@opentelemetry/api"; -import { logger } from "@sentry/utils"; import { GroupCall, MatrixClient, MatrixEvent, RoomMember, } from "matrix-js-sdk"; +import { logger } from "matrix-js-sdk/src/logger"; import { CallState, MatrixCall, From 848e28ef925de880dfa98e384f5321da0a44b31d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2023 15:51:07 +0100 Subject: [PATCH 15/43] Change allowed origin to https://* as that allows the PR branches out-of-the-box --- config/otel_dev/collector-gateway.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 7b1fad0..9c1a9cd 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -7,7 +7,8 @@ receivers: allowed_origins: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' - - "http://*" + #- "https://pr976--element-call.netlify.app" + - "https://*" allowed_headers: - "*" processors: From f96ce8985d261fecab22aed5b7be21c69ea6b9a9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Mar 2023 16:04:11 +0100 Subject: [PATCH 16/43] Only enable otel if we have a collector URL --- src/otel/otel.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/otel/otel.ts b/src/otel/otel.ts index 5d3e7d3..eac7ce4 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -88,12 +88,16 @@ export class ElementCallOpenTelemetry { } function recheckOTelEnabledStatus(optInAnalayticsEnabled: boolean): void { - if (optInAnalayticsEnabled && !sharedInstance) { + const shouldEnable = + optInAnalayticsEnabled && + Boolean(Config.get().opentelemetry?.collector_url); + + if (shouldEnable && !sharedInstance) { logger.info("Starting OpenTelemetry debug reporting"); sharedInstance = new ElementCallOpenTelemetry( Config.get().opentelemetry?.collector_url ); - } else if (!optInAnalayticsEnabled && sharedInstance) { + } else if (!shouldEnable && sharedInstance) { logger.info("Stopping OpenTelemetry debug reporting"); sharedInstance = undefined; } From 21458c8840bdf0cd71b619e2a7911bfc0f93f4b0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Mar 2023 13:03:58 +0100 Subject: [PATCH 17/43] Call the same leave method everywhere So we end the group call span whenever we leasve the call, including if we close the page. --- src/room/useGroupCall.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 392445f..b591629 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -202,6 +202,11 @@ export function useGroupCall( [] ); + const leaveCall = useCallback(() => { + groupCallOTelMembership?.onLeaveCall(); + groupCall.leave(); + }, [groupCall]); + useEffect(() => { // disable the media action keys, otherwise audio elements get paused when // the user presses media keys or unplugs headphones, etc. @@ -394,12 +399,12 @@ export function useGroupCall( onParticipantsChanged ); groupCall.removeListener(GroupCallEvent.Error, onError); - groupCall.leave(); + leaveCall(); }; - }, [groupCall, updateState]); + }, [groupCall, updateState, leaveCall]); usePageUnload(() => { - groupCall.leave(); + leaveCall(); }); const initLocalCallFeed = useCallback( @@ -426,11 +431,6 @@ export function useGroupCall( groupCallOTelMembership?.onJoinCall(); }, [groupCall, updateState]); - const leave = useCallback(() => { - groupCallOTelMembership?.onLeaveCall(); - groupCall.leave(); - }, [groupCall]); - const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); groupCall.setLocalVideoMuted(toggleToMute); @@ -563,7 +563,7 @@ export function useGroupCall( error, initLocalCallFeed, enter, - leave, + leave: leaveCall, toggleLocalVideoMuted, toggleMicrophoneMuted, toggleScreensharing, From c2b78d59c6aba34f9cc1f9f98dbc778b2708680d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Mar 2023 16:54:10 +0100 Subject: [PATCH 18/43] Add more events: * VoIP events received * Call errors * Group call errors * Undecryptable to-device events --- src/otel/OTelGroupCallMembership.ts | 53 +++++++++++++++++++++++++++++ src/room/GroupCallInspector.tsx | 24 ++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 432970b..53551b0 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -23,12 +23,14 @@ import { } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/src/logger"; import { + CallError, CallState, MatrixCall, VoipEvent, } from "matrix-js-sdk/src/webrtc/call"; import { CallsByUserAndDevice, + GroupCallError, GroupCallEvent, } from "matrix-js-sdk/src/webrtc/groupCall"; @@ -216,6 +218,37 @@ export class OTelGroupCallMembership { } } + public onReceivedVoipEvent(event: MatrixEvent) { + // These come straight from CallEventHandler so don't have + // a call already associated (in principle we could receive + // events for calls we don't know about). + const callId = event.getContent().call_id; + if (!callId) { + this.callMembershipSpan?.addEvent("matrix.receive_voip_event_no_callid", { + "sender.userId": event.getSender(), + }); + logger.error("Received call event with no call ID!"); + return; + } + + const call = this.callsByCallId.get(callId); + if (!call) { + this.callMembershipSpan?.addEvent( + "matrix.receive_voip_event_unknown_callid", + { + "sender.userId": event.getSender(), + } + ); + logger.error("Received call event for unknown call ID " + callId); + return; + } + + call.span.addEvent("matrix.receive_voip_event", { + "sender.userId": event.getSender(), + ...flattenVoipEvent(event.getContent()), + }); + } + public onToggleMicrophoneMuted(newValue: boolean) { this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", { "matrix.microphone.muted": newValue, @@ -245,4 +278,24 @@ export class OTelGroupCallMembership { "matrix.screensharing.enabled": newValue, }); } + + public onCallError(error: CallError, call: MatrixCall) { + const callTrackingInfo = this.callsByCallId.get(call.callId); + if (!callTrackingInfo) { + logger.error(`Got error for unknown call ID ${call.callId}`); + return; + } + + callTrackingInfo.span.recordException(error); + } + + public onGroupCallError(error: GroupCallError) { + this.callMembershipSpan?.recordException(error); + } + + public onUndecryptableToDevice(event: MatrixEvent) { + this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", { + "sender.userId": event.getSender(), + }); + } } diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index ad838fb..ad67c7a 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -28,12 +28,17 @@ import ReactJson, { CollapsedFieldProps } from "react-json-view"; import mermaid from "mermaid"; import { Item } from "@react-stately/collections"; import { MatrixEvent, IContent } from "matrix-js-sdk/src/models/event"; -import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; +import { + GroupCall, + GroupCallError, + GroupCallEvent, +} from "matrix-js-sdk/src/webrtc/groupCall"; import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { CallEvent, CallState, + CallError, MatrixCall, VoipEvent, } from "matrix-js-sdk/src/webrtc/call"; @@ -393,6 +398,8 @@ function useGroupCallState( function onReceivedVoipEvent(event: MatrixEvent) { dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); + + otelGroupCallMembership?.onReceivedVoipEvent(event); } function onSendVoipEvent(event: VoipEvent, call: MatrixCall) { @@ -409,18 +416,31 @@ function useGroupCallState( otelGroupCallMembership?.onCallStateChange(call, newState); } + function onCallError(error: CallError, call: MatrixCall) { + otelGroupCallMembership.onCallError(error, call); + } + + function onGroupCallError(error: GroupCallError) { + otelGroupCallMembership.onGroupCallError(error); + } + function onUndecryptableToDevice(event: MatrixEvent) { dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); Sentry.captureMessage("Undecryptable to-device Event"); + // probably unnecessary if it's now captured via otel? PosthogAnalytics.instance.eventUndecryptableToDevice.track( groupCall.groupCallId ); + + otelGroupCallMembership.onUndecryptableToDevice(event); } client.on(RoomStateEvent.Events, onUpdateRoomState); groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent); groupCall.on(CallEvent.State, onCallStateChange); + groupCall.on(CallEvent.Error, onCallError); + groupCall.on(GroupCallEvent.Error, onGroupCallError); //client.on("state", onCallsChanged); //client.on("hangup", onCallHangup); client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); @@ -432,6 +452,8 @@ function useGroupCallState( client.removeListener(RoomStateEvent.Events, onUpdateRoomState); groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent); groupCall.removeListener(CallEvent.State, onCallStateChange); + groupCall.removeListener(CallEvent.Error, onCallError); + groupCall.removeListener(GroupCallEvent.Error, onGroupCallError); //client.removeListener("state", onCallsChanged); //client.removeListener("hangup", onCallHangup); client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent); From 74b218af8c436ff18e336db09901c91de095a56e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Mar 2023 17:19:13 +0100 Subject: [PATCH 19/43] Let otel know we're joining before trying to join Otherwise it starts getting calls being created before the group call span exists and we get call spans not associated with the group call span. --- config/otel_dev/collector-gateway.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 9c1a9cd..f9e3b90 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -8,7 +8,7 @@ receivers: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' #- "https://pr976--element-call.netlify.app" - - "https://*" + - "http://*" allowed_headers: - "*" processors: From 72403d1aeac2bd17e363ea39439e8086de1919f7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 10:26:33 +0100 Subject: [PATCH 20/43] Revert 74b218af8c436ff18e336db09901c91de095a56e Comitted entirely the wrong thing --- config/otel_dev/collector-gateway.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index f9e3b90..9c1a9cd 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -8,7 +8,7 @@ receivers: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' #- "https://pr976--element-call.netlify.app" - - "http://*" + - "https://*" allowed_headers: - "*" processors: From 5e6c33b3b5aba9e776d217a3a33f3041e6f9fddf Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 10:30:01 +0100 Subject: [PATCH 21/43] Let otel know we're joining before trying to join Otherwise it starts getting calls being created before the group call span exists and we get call spans not associated with the group call span. (What 74b218af8c436ff18e336db09901c91de095a56e should have been) --- src/room/useGroupCall.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index b591629..7a9f69a 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -423,12 +423,14 @@ export function useGroupCall( PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); + // This must be called before we start trying to join the call, as we need to + // have started tracking by the time calls start getting created. + groupCallOTelMembership?.onJoinCall(); + groupCall.enter().catch((error) => { console.error(error); updateState({ error }); }); - - groupCallOTelMembership?.onJoinCall(); }, [groupCall, updateState]); const toggleLocalVideoMuted = useCallback(() => { From 773f2e009de5850ccce00d24e2f8c47d88391423 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 10:58:12 +0100 Subject: [PATCH 22/43] Typo --- config/otel_dev/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md index 8fe102d..ea6c09a 100644 --- a/config/otel_dev/README.md +++ b/config/otel_dev/README.md @@ -1,7 +1,7 @@ # OpenTelemetry Collector for development This directory contains a docker compose file that starts a jaeger all-in-one instance -with an in-memory database, along with a standalong OpenTelemetry collector that forwards +with an in-memory database, along with a standalone OpenTelemetry collector that forwards traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be configured to send CORS headers so can't be used from a browser. This sets the config on the collector to send CORS headers. From a1aca7bdf234214fc2dc144ccf5472eb0420f004 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 11:10:05 +0100 Subject: [PATCH 23/43] Fix lying comment --- src/analytics/OtelPosthogExporter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index 4fbfa90..8f9ba26 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -18,9 +18,10 @@ import { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; import { PosthogAnalytics } from "./PosthogAnalytics"; + /** - * This is implementation of {@link SpanExporter} that prints spans to the - * console. This class can be used for diagnostic purposes. + * This is implementation of {@link SpanExporter} that sends spans + * to Posthog */ export class PosthogSpanExporter implements SpanExporter { /** From dc725f90a9b8971e198cfb07c42ec6d5fbcb5ff5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 11:12:10 +0100 Subject: [PATCH 24/43] Fix confusing comment --- src/config/ConfigOptions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index cfaaf77..8b0fae1 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -37,7 +37,8 @@ export interface ConfigOptions { }; /** - * Controls whether to to send OpenTelemetry debugging data to collector + * Sets the URL to send opentelemetry data to. If unset, opentelemetry will + * be disabled. */ opentelemetry?: { collector_url: string; From dd67a45671d2d318f5bd77d494039a394f34e477 Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Fri, 31 Mar 2023 14:57:56 +0200 Subject: [PATCH 25/43] stats: Add summery report --- config/otel_dev/collector-gateway.yaml | 2 +- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 56 ++++++++++++++++---------- src/otel/ObjectFlattener.ts | 14 +++++++ src/room/useGroupCall.ts | 21 ++++++++-- yarn.lock | 31 +++++++------- 6 files changed, 82 insertions(+), 44 deletions(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 9c1a9cd..f9e3b90 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -8,7 +8,7 @@ receivers: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' #- "https://pr976--element-call.netlify.app" - - "https://*" + - "http://*" allowed_headers: - "*" processors: diff --git a/package.json b/package.json index f7fc0e1..2bec81e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6339dc8cae35e501032666da58b842793ae94fad", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 2685209..2c9a016 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -37,10 +37,10 @@ import { import { ConnectionStatsReport, ByteSentStatsReport, + SummeryStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils"; - import { ElementCallOpenTelemetry } from "./otel"; import { ObjectFlattener } from "./ObjectFlattener"; @@ -117,7 +117,7 @@ export class OTelGroupCallMembership { this.myMember = myMember; } } - this.myDeviceId = client.getDeviceId(); + this.myDeviceId = client.getDeviceId() || "unknown"; this.statsReportSpan = { span: undefined, stats: [] }; this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); } @@ -317,55 +317,68 @@ export class OTelGroupCallMembership { }); } - public onConnectionStatsReport( - statsReport: GroupCallStatsReport + statsReport: GroupCallStatsReport ) { const type = OTelStatsReportType.ConnectionStatsReport; const data = - ObjectFlattener.flattenConnectionStatsReportObject(statsReport); + ObjectFlattener.flattenConnectionStatsReportObject(statsReport); this.buildStatsEventSpan({ type, data }); } public onByteSentStatsReport( - statsReport: GroupCallStatsReport + statsReport: GroupCallStatsReport ) { const type = OTelStatsReportType.ByteSentStatsReport; const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport); this.buildStatsEventSpan({ type, data }); } + public onSummeryStatsReport( + statsReport: GroupCallStatsReport + ) { + const type = OTelStatsReportType.SummeryStatsReport; + const data = ObjectFlattener.flattenSummeryStatsReportObject(statsReport); + this.buildStatsEventSpan({ type, data }); + } + private buildStatsEventSpan(event: OTelStatsReportEvent): void { + // @ TODO: fix this - Because on multiple calls we receive multiple stats report spans. + // This could be break if stats arrived in same time from different call objects. if (this.statsReportSpan.span === undefined && this.callMembershipSpan) { const ctx = setSpan( - opentelemetry.context.active(), - this.callMembershipSpan + opentelemetry.context.active(), + this.callMembershipSpan ); this.statsReportSpan.span = - ElementCallOpenTelemetry.instance.tracer.startSpan( - "matrix.groupCallMembership.statsReport", - undefined, - ctx - ); + ElementCallOpenTelemetry.instance.tracer.startSpan( + "matrix.groupCallMembership.statsReport", + undefined, + ctx + ); this.statsReportSpan.span.setAttribute( - "matrix.confId", - this.groupCall.groupCallId + "matrix.confId", + this.groupCall.groupCallId ); this.statsReportSpan.span.setAttribute("matrix.userId", this.myUserId); this.statsReportSpan.span.setAttribute( - "matrix.displayName", - this.myMember ? this.myMember.name : "unknown-name" + "matrix.displayName", + this.myMember ? this.myMember.name : "unknown-name" ); this.statsReportSpan.span.addEvent(event.type, event.data); this.statsReportSpan.stats.push(event); } else if ( - this.statsReportSpan.span !== undefined && - this.callMembershipSpan + this.statsReportSpan.span !== undefined && + this.callMembershipSpan ) { this.statsReportSpan.span.addEvent(event.type, event.data); - this.statsReportSpan.span.end(); - this.statsReportSpan = { span: undefined, stats: [] }; + this.statsReportSpan.stats.push(event); + // if received all three types of stats close this + if (this.statsReportSpan.stats.length === 3) { + this.statsReportSpan.span.end(); + this.statsReportSpan = { span: undefined, stats: [] }; + } } } } @@ -378,4 +391,5 @@ interface OTelStatsReportEvent { enum OTelStatsReportType { ConnectionStatsReport = "matrix.stats.connection", ByteSentStatsReport = "matrix.stats.byteSent", + SummeryStatsReport = "matrix.stats.summery", } diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts index d45360c..c332aba 100644 --- a/src/otel/ObjectFlattener.ts +++ b/src/otel/ObjectFlattener.ts @@ -18,6 +18,7 @@ import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; import { ByteSentStatsReport, ConnectionStatsReport, + SummeryStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; export class ObjectFlattener { @@ -47,6 +48,19 @@ export class ObjectFlattener { return flatObject; } + static flattenSummeryStatsReportObject( + statsReport: GroupCallStatsReport + ) { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report, + flatObject, + "matrix.stats.summery.", + 0 + ); + return flatObject; + } + public static flattenObjectRecursive( obj: Object, flatObject: Attributes, diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 030219b..52fc31b 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -33,6 +33,7 @@ import { MatrixClient } from "matrix-js-sdk"; import { ByteSentStatsReport, ConnectionStatsReport, + SummeryStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { usePageUnload } from "./usePageUnload"; @@ -355,6 +356,12 @@ export function useGroupCall( groupCallOTelMembership?.onByteSentStatsReport(report); } + function onSummeryStatsReport( + report: GroupCallStatsReport + ): void { + groupCallOTelMembership?.onSummeryStatsReport(report); + } + groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged); groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged); groupCall.on( @@ -381,6 +388,8 @@ export function useGroupCall( onByteSentStatsReport ); + groupCall.on(GroupCallStatsReportEvent.SummeryStats, onSummeryStatsReport); + updateState({ error: null, state: groupCall.state, @@ -428,12 +437,16 @@ export function useGroupCall( ); groupCall.removeListener(GroupCallEvent.Error, onError); groupCall.removeListener( - GroupCallStatsReportEvent.ConnectionStats, - onConnectionStatsReport + GroupCallStatsReportEvent.ConnectionStats, + onConnectionStatsReport ); groupCall.removeListener( - GroupCallStatsReportEvent.ByteSentStats, - onByteSentStatsReport + GroupCallStatsReportEvent.ByteSentStats, + onByteSentStatsReport + ); + groupCall.removeListener( + GroupCallStatsReportEvent.SummeryStats, + onSummeryStatsReport ); leaveCall(); }; diff --git a/yarn.lock b/yarn.lock index e25096f..66c1dc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1822,9 +1822,9 @@ integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== "@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.5": - version "0.1.0-alpha.5" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" - integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== + version "0.1.0-alpha.6" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.6.tgz#c0bdb9ab0d30179b8ef744d1b4010b0ad0ab9c3a" + integrity sha512-7hMffzw7KijxDyyH/eUyTfrLeCQHuyU3kaPOKGhcl3DZ3vx7bCncqjGMGTnxNPoP23I6gosvKSbO+3wYOT24Xg== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -5726,7 +5726,12 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@^1.0.4, content-type@~1.0.4: +content-type@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== @@ -10413,9 +10418,9 @@ log-symbols@^4.1.0: is-unicode-supported "^0.1.0" loglevel@^1.7.1: - version "1.8.0" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" - integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== + version "1.8.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" + integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== long@^2.4.0: version "2.4.0" @@ -10545,9 +10550,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#6339dc8cae35e501032666da58b842793ae94fad": version "24.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/042f2ed76c501c10dde98a31732fd92d862e2187" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6339dc8cae35e501032666da58b842793ae94fad" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" @@ -10570,14 +10575,6 @@ matrix-widget-api@^1.3.1: "@types/events" "^3.0.0" events "^3.2.0" -matrix-widget-api@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" - integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== - dependencies: - "@types/events" "^3.0.0" - events "^3.2.0" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" From 47e0ca2eda0c4616ed777b498b50b4b2fee2a6cf Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 14:27:50 +0100 Subject: [PATCH 26/43] Put cors header back to https for now To remove that change for the diff --- config/otel_dev/collector-gateway.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index f9e3b90..9c1a9cd 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -8,7 +8,7 @@ receivers: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' #- "https://pr976--element-call.netlify.app" - - "http://*" + - "https://*" allowed_headers: - "*" processors: From e18c69ec89f60470413f2daf9fc2d08c18960f6b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 14:29:07 +0100 Subject: [PATCH 27/43] Use latest js-sdk develop --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2bec81e..2e4fa35 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6339dc8cae35e501032666da58b842793ae94fad", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#d1cf98b1770d0282224e80bc9303609f6c156d3a", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index 66c1dc9..595cae5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10550,9 +10550,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#6339dc8cae35e501032666da58b842793ae94fad": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#d1cf98b1770d0282224e80bc9303609f6c156d3a": version "24.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6339dc8cae35e501032666da58b842793ae94fad" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d1cf98b1770d0282224e80bc9303609f6c156d3a" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" From cb0ba6d8277506675f9a641f29b0b8c130f5b8d6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Mar 2023 14:30:24 +0100 Subject: [PATCH 28/43] Add missed 'r' --- test/otel/{ObjectFlattene-test.ts => ObjectFlattener-test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/otel/{ObjectFlattene-test.ts => ObjectFlattener-test.ts} (100%) diff --git a/test/otel/ObjectFlattene-test.ts b/test/otel/ObjectFlattener-test.ts similarity index 100% rename from test/otel/ObjectFlattene-test.ts rename to test/otel/ObjectFlattener-test.ts From 889a31489bfb5afa878e57e1f1939d88f76641b1 Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Mon, 3 Apr 2023 12:37:55 +0200 Subject: [PATCH 29/43] stats: fix typo --- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 12 ++++++------ src/otel/ObjectFlattener.ts | 8 ++++---- src/room/useGroupCall.ts | 14 +++++++------- yarn.lock | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 2e4fa35..86a666f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#d1cf98b1770d0282224e80bc9303609f6c156d3a", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#fe79a6fa7ca50fc7d078e11826b5539bb0822c45", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 2c9a016..78f171f 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -37,7 +37,7 @@ import { import { ConnectionStatsReport, ByteSentStatsReport, - SummeryStatsReport, + SummaryStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils"; @@ -334,11 +334,11 @@ export class OTelGroupCallMembership { this.buildStatsEventSpan({ type, data }); } - public onSummeryStatsReport( - statsReport: GroupCallStatsReport + public onSummaryStatsReport( + statsReport: GroupCallStatsReport ) { - const type = OTelStatsReportType.SummeryStatsReport; - const data = ObjectFlattener.flattenSummeryStatsReportObject(statsReport); + const type = OTelStatsReportType.SummaryStatsReport; + const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport); this.buildStatsEventSpan({ type, data }); } @@ -391,5 +391,5 @@ interface OTelStatsReportEvent { enum OTelStatsReportType { ConnectionStatsReport = "matrix.stats.connection", ByteSentStatsReport = "matrix.stats.byteSent", - SummeryStatsReport = "matrix.stats.summery", + SummaryStatsReport = "matrix.stats.summary", } diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts index c332aba..dcda078 100644 --- a/src/otel/ObjectFlattener.ts +++ b/src/otel/ObjectFlattener.ts @@ -18,7 +18,7 @@ import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; import { ByteSentStatsReport, ConnectionStatsReport, - SummeryStatsReport, + SummaryStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; export class ObjectFlattener { @@ -48,14 +48,14 @@ export class ObjectFlattener { return flatObject; } - static flattenSummeryStatsReportObject( - statsReport: GroupCallStatsReport + static flattenSummaryStatsReportObject( + statsReport: GroupCallStatsReport ) { const flatObject = {}; ObjectFlattener.flattenObjectRecursive( statsReport.report, flatObject, - "matrix.stats.summery.", + "matrix.stats.summary.", 0 ); return flatObject; diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 52fc31b..48b8d8f 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -33,7 +33,7 @@ import { MatrixClient } from "matrix-js-sdk"; import { ByteSentStatsReport, ConnectionStatsReport, - SummeryStatsReport, + SummaryStatsReport, } from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { usePageUnload } from "./usePageUnload"; @@ -356,10 +356,10 @@ export function useGroupCall( groupCallOTelMembership?.onByteSentStatsReport(report); } - function onSummeryStatsReport( - report: GroupCallStatsReport + function onSummaryStatsReport( + report: GroupCallStatsReport ): void { - groupCallOTelMembership?.onSummeryStatsReport(report); + groupCallOTelMembership?.onSummaryStatsReport(report); } groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged); @@ -388,7 +388,7 @@ export function useGroupCall( onByteSentStatsReport ); - groupCall.on(GroupCallStatsReportEvent.SummeryStats, onSummeryStatsReport); + groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport); updateState({ error: null, @@ -445,8 +445,8 @@ export function useGroupCall( onByteSentStatsReport ); groupCall.removeListener( - GroupCallStatsReportEvent.SummeryStats, - onSummeryStatsReport + GroupCallStatsReportEvent.SummaryStats, + onSummaryStatsReport ); leaveCall(); }; diff --git a/yarn.lock b/yarn.lock index 595cae5..d98a26f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10550,9 +10550,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#d1cf98b1770d0282224e80bc9303609f6c156d3a": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#fe79a6fa7ca50fc7d078e11826b5539bb0822c45": version "24.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d1cf98b1770d0282224e80bc9303609f6c156d3a" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fe79a6fa7ca50fc7d078e11826b5539bb0822c45" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" From 3b06258e40c7e83725520627b474874cc315ada5 Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Mon, 3 Apr 2023 14:07:29 +0200 Subject: [PATCH 30/43] stats: rename enum to avoid shadow values --- src/otel/OTelGroupCallMembership.ts | 12 ++++++------ src/otel/otel.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 78f171f..45d5bfd 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -320,7 +320,7 @@ export class OTelGroupCallMembership { public onConnectionStatsReport( statsReport: GroupCallStatsReport ) { - const type = OTelStatsReportType.ConnectionStatsReport; + const type = OTelStatsReportType.ConnectionReport; const data = ObjectFlattener.flattenConnectionStatsReportObject(statsReport); this.buildStatsEventSpan({ type, data }); @@ -329,7 +329,7 @@ export class OTelGroupCallMembership { public onByteSentStatsReport( statsReport: GroupCallStatsReport ) { - const type = OTelStatsReportType.ByteSentStatsReport; + const type = OTelStatsReportType.ByteSentReport; const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport); this.buildStatsEventSpan({ type, data }); } @@ -337,7 +337,7 @@ export class OTelGroupCallMembership { public onSummaryStatsReport( statsReport: GroupCallStatsReport ) { - const type = OTelStatsReportType.SummaryStatsReport; + const type = OTelStatsReportType.SummaryReport; const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport); this.buildStatsEventSpan({ type, data }); } @@ -389,7 +389,7 @@ interface OTelStatsReportEvent { } enum OTelStatsReportType { - ConnectionStatsReport = "matrix.stats.connection", - ByteSentStatsReport = "matrix.stats.byteSent", - SummaryStatsReport = "matrix.stats.summary", + ConnectionReport = "matrix.stats.connection", + ByteSentReport = "matrix.stats.byteSent", + SummaryReport = "matrix.stats.summary", } diff --git a/src/otel/otel.ts b/src/otel/otel.ts index eac7ce4..d079da4 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -48,7 +48,7 @@ export class ElementCallOpenTelemetry { return sharedInstance; } - constructor(collectorUrl: string) { + constructor(collectorUrl: string | undefined) { const otlpExporter = new OTLPTraceExporter({ url: collectorUrl, }); From 3a7983d2de2e160f009d04f9764ed3e3b144e61f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Mar 2023 14:07:49 +0100 Subject: [PATCH 31/43] Add displayname on call spans --- src/otel/OTelGroupCallMembership.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 53551b0..2ec11bd 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -170,6 +170,9 @@ export class OTelGroupCallMembership { // XXX: anonymity span.setAttribute("matrix.call.target.userId", userId); span.setAttribute("matrix.call.target.deviceId", deviceId); + + const displayName = this.groupCall.room.getMember(userId)?.name; + span.setAttribute("matrix.call.target.displayName", displayName); this.callsByCallId.set(call.callId, { userId, deviceId, From 277081ee2a82cac68d24a6e87e0dab66e35bf2c7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Mar 2023 13:51:12 +0100 Subject: [PATCH 32/43] Move call events to the call span --- src/otel/OTelGroupCallMembership.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 53551b0..3c73916 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -205,13 +205,15 @@ export class OTelGroupCallMembership { const eventType = event.eventType as string; if (!eventType.startsWith("m.call")) return; + const callTrackingInfo = this.callsByCallId.get(call.callId); + if (event.type === "toDevice") { - this.callMembershipSpan?.addEvent( + callTrackingInfo.span.addEvent( `matrix.sendToDeviceEvent_${event.eventType}`, flattenVoipEvent(event) ); } else if (event.type === "sendEvent") { - this.callMembershipSpan?.addEvent( + callTrackingInfo.span.addEvent( `matrix.sendToRoomEvent_${event.eventType}`, flattenVoipEvent(event) ); From 8fa23b7da93ea809dc6b63908051f3f6f14d017d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 3 Apr 2023 16:58:29 +0100 Subject: [PATCH 33/43] Include booleans in flattened OpenTelemetry object --- src/otel/OTelGroupCallMembership.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 53551b0..95112a1 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -65,7 +65,7 @@ function flattenVoipEventRecursive( ); for (const [k, v] of Object.entries(obj)) { - if (["string", "number"].includes(typeof v)) { + if (["string", "number", "boolean"].includes(typeof v)) { flatObject[prefix + k] = v; } else if (typeof v === "object") { flattenVoipEventRecursive( From 30f75c6cd224d0eb9410d56b5f0736f0a43b0380 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 3 Apr 2023 17:41:40 +0100 Subject: [PATCH 34/43] Don't pass null / undefined as attribute value --- config/otel_dev/collector-gateway.yaml | 2 +- src/otel/OTelGroupCallMembership.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml index 9c1a9cd..f9e3b90 100644 --- a/config/otel_dev/collector-gateway.yaml +++ b/config/otel_dev/collector-gateway.yaml @@ -8,7 +8,7 @@ receivers: # This can't be '*' because opentelemetry-js uses sendBeacon which always operates # in 'withCredentials' mode, which browsers don't allow with an allow-origin of '*' #- "https://pr976--element-call.netlify.app" - - "https://*" + - "http://*" allowed_headers: - "*" processors: diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 2ec11bd..69bf024 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -171,7 +171,8 @@ export class OTelGroupCallMembership { span.setAttribute("matrix.call.target.userId", userId); span.setAttribute("matrix.call.target.deviceId", deviceId); - const displayName = this.groupCall.room.getMember(userId)?.name; + const displayName = + this.groupCall.room.getMember(userId)?.name ?? "unknown"; span.setAttribute("matrix.call.target.displayName", displayName); this.callsByCallId.set(call.callId, { userId, From a52251befab5db50bd4c2373406c301a27e5d265 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 3 Apr 2023 20:57:03 -0400 Subject: [PATCH 35/43] Track call rejoins Call rejoins will be one of the KPIs we track in PostHog to measure call quality. I've also reverted the previous behavior which logged all OpenTelemetry spans to PostHog, since we should only be sending small, anonymized bits of data there. --- src/analytics/OtelPosthogExporter.ts | 106 +++++++++++++++++++-------- src/analytics/PosthogAnalytics.ts | 16 ---- 2 files changed, 75 insertions(+), 47 deletions(-) diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts index 8f9ba26..c624a00 100644 --- a/src/analytics/OtelPosthogExporter.ts +++ b/src/analytics/OtelPosthogExporter.ts @@ -16,12 +16,29 @@ limitations under the License. import { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"; import { ExportResult, ExportResultCode } from "@opentelemetry/core"; +import { logger } from "matrix-js-sdk/src/logger"; +import { HrTime } from "@opentelemetry/api"; import { PosthogAnalytics } from "./PosthogAnalytics"; +interface PrevCall { + callId: string; + hangupTs: number; +} + +function hrTimeToMs(time: HrTime): number { + return time[0] * 1000 + time[1] * 0.000001; +} + /** - * This is implementation of {@link SpanExporter} that sends spans - * to Posthog + * The maximum time between hanging up and joining the same call that we would + * consider a 'rejoin' on the user's part. + */ +const maxRejoinMs = 2 * 60 * 1000; // 2 minutes + +/** + * This is implementation of {@link SpanExporter} that extracts certain metrics + * from spans to send to PostHog */ export class PosthogSpanExporter implements SpanExporter { /** @@ -33,41 +50,68 @@ export class PosthogSpanExporter implements SpanExporter { spans: ReadableSpan[], resultCallback: (result: ExportResult) => void ): Promise { - console.log("POSTHOGEXPORTER", spans); - for (const span of spans) { - const sendInstantly = [ - "otel_callEnded", - "otel_otherSentInstantlyEventName", - ].includes(span.name); - - for (const spanEvent of span.events) { - await PosthogAnalytics.instance.trackFromSpan( - { - eventName: spanEvent.name, - ...spanEvent.attributes, - }, - { - send_instantly: sendInstantly, - } - ); - } - - await PosthogAnalytics.instance.trackFromSpan( - { eventName: span.name, ...span.attributes }, - { - send_instantly: sendInstantly, + await Promise.all( + spans.map((span) => { + switch (span.name) { + case "matrix.groupCallMembership": + return this.exportGroupCallMembershipSpan(span); + // TBD if there are other spans that we want to process for export to + // PostHog } - ); - resultCallback({ code: ExportResultCode.SUCCESS }); + }) + ); + + resultCallback({ code: ExportResultCode.SUCCESS }); + } + + private get prevCall(): PrevCall | null { + // This is stored in localStorage so we can remember the previous call + // across app restarts + const data = localStorage.getItem("matrix-prev-call"); + if (data === null) return null; + + try { + return JSON.parse(data); + } catch (e) { + logger.warn("Invalid prev call data", data); + return null; } } + + private set prevCall(data: PrevCall | null) { + localStorage.setItem("matrix-prev-call", JSON.stringify(data)); + } + + async exportGroupCallMembershipSpan(span: ReadableSpan): Promise { + const prevCall = this.prevCall; + const newPrevCall = (this.prevCall = { + callId: span.attributes["matrix.confId"] as string, + hangupTs: hrTimeToMs(span.endTime), + }); + + // If the user joined the same call within a short time frame, log this as a + // rejoin. This is interesting as a call quality metric, since rejoins may + // indicate that users had to intervene to make the product work. + if (prevCall !== null && newPrevCall.callId === prevCall.callId) { + const duration = hrTimeToMs(span.startTime) - prevCall.hangupTs; + if (duration <= maxRejoinMs) { + PosthogAnalytics.instance.trackEvent( + { + eventName: "Rejoin", + callId: prevCall.callId, + rejoinDuration: duration, + }, + // Send instantly because the window might be closing + { send_instantly: true } + ); + } + } + } + /** * Shutdown the exporter. */ shutdown(): Promise { - console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter"); - return new Promise((resolve, _reject) => { - resolve(); - }); + return Promise.resolve(); } } diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 718a49c..e2e8fda 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -385,22 +385,6 @@ export class PosthogAnalytics { this.capture(eventName, properties, options); } - public async trackFromSpan( - { eventName, ...properties }, - options?: CaptureOptions - ): Promise { - if (this.identificationPromise) { - // only make calls to posthog after the identificaion is done - await this.identificationPromise; - } - if ( - this.anonymity == Anonymity.Disabled || - this.anonymity == Anonymity.Anonymous - ) - return; - this.capture(eventName, properties, options); - } - public startListeningToSettingsChanges(): void { // Listen to account data changes from sync so we can observe changes to relevant flags and update. // This is called - From 5b70def4d20aa5712611f02666942b1cbc4d2a25 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 4 Apr 2023 17:49:49 +0100 Subject: [PATCH 36/43] Add null check for call span --- src/otel/OTelGroupCallMembership.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 3c73916..bd116f4 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -206,6 +206,10 @@ export class OTelGroupCallMembership { if (!eventType.startsWith("m.call")) return; const callTrackingInfo = this.callsByCallId.get(call.callId); + if (!callTrackingInfo) { + logger.error(`Got call send event for unknown call ID ${call.callId}`); + return; + } if (event.type === "toDevice") { callTrackingInfo.span.addEvent( From c824ea6f9a229d28bded3929fdeb8763323d4cc2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 4 Apr 2023 18:00:45 +0100 Subject: [PATCH 37/43] Add OpenTelemetry events for PeerConnection state changes / errors Creates a new class to represent individual calls and adds the listeners there. Requires https://github.com/matrix-org/matrix-js-sdk/pull/3251 Based on https://github.com/vector-im/element-call/pull/974 --- src/otel/OTelCall.ts | 103 ++++++++++++++++++++++++++++ src/otel/OTelGroupCallMembership.ts | 20 ++---- 2 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 src/otel/OTelCall.ts diff --git a/src/otel/OTelCall.ts b/src/otel/OTelCall.ts new file mode 100644 index 0000000..3717abf --- /dev/null +++ b/src/otel/OTelCall.ts @@ -0,0 +1,103 @@ +/* +Copyright 2023 New Vector Ltd + +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 { Span } from "@opentelemetry/api"; +import { MatrixCall } from "matrix-js-sdk"; +import { CallEvent } from "matrix-js-sdk/src/webrtc/call"; + +import { ObjectFlattener } from "./ObjectFlattener"; + +/** + * Tracks an individual call within a group call, either to a full-mesh peer or a focus + */ +export class OTelCall { + constructor( + public userId: string, + public deviceId: string, + public call: MatrixCall, + public span: Span + ) { + if (call.peerConn) { + this.addCallPeerConnListeners(); + } else { + this.call.once( + CallEvent.PeerConnectionCreated, + this.addCallPeerConnListeners + ); + } + } + + public dispose() { + this.call.peerConn.removeEventListener( + "iceconnectionstatechange", + this.onIceConnectionStateChanged + ); + } + + private addCallPeerConnListeners = (): void => { + this.call.peerConn.addEventListener( + "connectionstatechange", + this.onCallConnectionStateChanged + ); + this.call.peerConn.addEventListener( + "signalingstatechange", + this.onCallSignalingStateChanged + ); + this.call.peerConn.addEventListener( + "iceconnectionstatechange", + this.onIceConnectionStateChanged + ); + this.call.peerConn.addEventListener( + "icegatheringstatechange", + this.onIceGatheringStateChanged + ); + this.call.peerConn.addEventListener( + "icecandidateerror", + this.onIceCandidateError + ); + }; + + public onCallConnectionStateChanged = (): void => { + this.span.addEvent("matrix.call.callConnectionStateChange", { + callConnectionState: this.call.peerConn.connectionState, + }); + }; + + public onCallSignalingStateChanged = (): void => { + this.span.addEvent("matrix.call.callSignalingStateChange", { + callSignalingState: this.call.peerConn.signalingState, + }); + }; + + public onIceConnectionStateChanged = (): void => { + this.span.addEvent("matrix.call.iceConnectionStateChange", { + iceConnectionState: this.call.peerConn.iceConnectionState, + }); + }; + + public onIceGatheringStateChanged = (): void => { + this.span.addEvent("matrix.call.iceGatheringStateChange", { + iceGatheringState: this.call.peerConn.iceGatheringState, + }); + }; + + public onIceCandidateError = (ev: Event): void => { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive(ev, flatObject, "error.", 0); + + this.span.addEvent("matrix.call.iceCandidateError", flatObject); + }; +} diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 45d5bfd..6d3d3b7 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -43,6 +43,7 @@ import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils"; import { ElementCallOpenTelemetry } from "./otel"; import { ObjectFlattener } from "./ObjectFlattener"; +import { OTelCall } from "./OTelCall"; /** * Flattens out an object into a single layer with components @@ -86,13 +87,6 @@ function flattenVoipEventRecursive( } } -interface CallTrackingInfo { - userId: string; - deviceId: string; - call: MatrixCall; - span: Span; -} - /** * Represent the span of time which we intend to be joined to a group call */ @@ -102,7 +96,7 @@ export class OTelGroupCallMembership { private myUserId = "unknown"; private myDeviceId: string; private myMember?: RoomMember; - private callsByCallId = new Map(); + private callsByCallId = new Map(); private statsReportSpan: { span: Span | undefined; stats: OTelStatsReportEvent[]; @@ -188,12 +182,10 @@ export class OTelGroupCallMembership { // XXX: anonymity span.setAttribute("matrix.call.target.userId", userId); span.setAttribute("matrix.call.target.deviceId", deviceId); - this.callsByCallId.set(call.callId, { - userId, - deviceId, - call, - span, - }); + this.callsByCallId.set( + call.callId, + new OTelCall(userId, deviceId, call, span) + ); } } } From 390442a4c3729f38568d35446afb1b9737e0713f Mon Sep 17 00:00:00 2001 From: Enrico Schwendig Date: Wed, 5 Apr 2023 10:25:26 +0200 Subject: [PATCH 38/43] Add webrtc metric to OTel (#974) * stats: Add summery report --------- Co-authored-by: David Baker --- package.json | 2 +- src/otel/OTelGroupCallMembership.ts | 108 +++++++++++++- src/otel/ObjectFlattener.ts | 97 +++++++++++++ src/otel/otel.ts | 2 +- src/room/useGroupCall.ts | 49 +++++++ test/otel/ObjectFlattener-test.ts | 215 ++++++++++++++++++++++++++++ yarn.lock | 31 ++-- 7 files changed, 478 insertions(+), 26 deletions(-) create mode 100644 src/otel/ObjectFlattener.ts create mode 100644 test/otel/ObjectFlattener-test.ts diff --git a/package.json b/package.json index f7fc0e1..86a666f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#fe79a6fa7ca50fc7d078e11826b5539bb0822c45", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index b9b03ad..49c9354 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -32,9 +32,17 @@ import { CallsByUserAndDevice, GroupCallError, GroupCallEvent, + GroupCallStatsReport, } from "matrix-js-sdk/src/webrtc/groupCall"; +import { + ConnectionStatsReport, + ByteSentStatsReport, + SummaryStatsReport, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; +import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils"; import { ElementCallOpenTelemetry } from "./otel"; +import { ObjectFlattener } from "./ObjectFlattener"; /** * Flattens out an object into a single layer with components @@ -91,16 +99,26 @@ interface CallTrackingInfo { export class OTelGroupCallMembership { private callMembershipSpan?: Span; private groupCallContext?: Context; - private myUserId: string; + private myUserId = "unknown"; private myDeviceId: string; - private myMember: RoomMember; + private myMember?: RoomMember; private callsByCallId = new Map(); + private statsReportSpan: { + span: Span | undefined; + stats: OTelStatsReportEvent[]; + }; constructor(private groupCall: GroupCall, client: MatrixClient) { - this.myUserId = client.getUserId(); - this.myDeviceId = client.getDeviceId(); - this.myMember = groupCall.room.getMember(client.getUserId()); - + const clientId = client.getUserId(); + if (clientId) { + this.myUserId = clientId; + const myMember = groupCall.room.getMember(clientId); + if (myMember) { + this.myMember = myMember; + } + } + this.myDeviceId = client.getDeviceId() || "unknown"; + this.statsReportSpan = { span: undefined, stats: [] }; this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); } @@ -125,7 +143,7 @@ export class OTelGroupCallMembership { this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId); this.callMembershipSpan.setAttribute( "matrix.displayName", - this.myMember.name + this.myMember ? this.myMember.name : "unknown-name" ); this.groupCallContext = opentelemetry.trace.setSpan( @@ -308,4 +326,80 @@ export class OTelGroupCallMembership { "sender.userId": event.getSender(), }); } + + public onConnectionStatsReport( + statsReport: GroupCallStatsReport + ) { + const type = OTelStatsReportType.ConnectionReport; + const data = + ObjectFlattener.flattenConnectionStatsReportObject(statsReport); + this.buildStatsEventSpan({ type, data }); + } + + public onByteSentStatsReport( + statsReport: GroupCallStatsReport + ) { + const type = OTelStatsReportType.ByteSentReport; + const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport); + this.buildStatsEventSpan({ type, data }); + } + + public onSummaryStatsReport( + statsReport: GroupCallStatsReport + ) { + const type = OTelStatsReportType.SummaryReport; + const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport); + this.buildStatsEventSpan({ type, data }); + } + + private buildStatsEventSpan(event: OTelStatsReportEvent): void { + // @ TODO: fix this - Because on multiple calls we receive multiple stats report spans. + // This could be break if stats arrived in same time from different call objects. + if (this.statsReportSpan.span === undefined && this.callMembershipSpan) { + const ctx = setSpan( + opentelemetry.context.active(), + this.callMembershipSpan + ); + this.statsReportSpan.span = + ElementCallOpenTelemetry.instance.tracer.startSpan( + "matrix.groupCallMembership.statsReport", + undefined, + ctx + ); + this.statsReportSpan.span.setAttribute( + "matrix.confId", + this.groupCall.groupCallId + ); + this.statsReportSpan.span.setAttribute("matrix.userId", this.myUserId); + this.statsReportSpan.span.setAttribute( + "matrix.displayName", + this.myMember ? this.myMember.name : "unknown-name" + ); + + this.statsReportSpan.span.addEvent(event.type, event.data); + this.statsReportSpan.stats.push(event); + } else if ( + this.statsReportSpan.span !== undefined && + this.callMembershipSpan + ) { + this.statsReportSpan.span.addEvent(event.type, event.data); + this.statsReportSpan.stats.push(event); + // if received all three types of stats close this + if (this.statsReportSpan.stats.length === 3) { + this.statsReportSpan.span.end(); + this.statsReportSpan = { span: undefined, stats: [] }; + } + } + } +} + +interface OTelStatsReportEvent { + type: OTelStatsReportType; + data: Attributes; +} + +enum OTelStatsReportType { + ConnectionReport = "matrix.stats.connection", + ByteSentReport = "matrix.stats.byteSent", + SummaryReport = "matrix.stats.summary", } diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts new file mode 100644 index 0000000..dcda078 --- /dev/null +++ b/src/otel/ObjectFlattener.ts @@ -0,0 +1,97 @@ +/* +Copyright 2023 New Vector Ltd + +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 { Attributes } from "@opentelemetry/api"; +import { GroupCallStatsReport } from "matrix-js-sdk/src/webrtc/groupCall"; +import { + ByteSentStatsReport, + ConnectionStatsReport, + SummaryStatsReport, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; + +export class ObjectFlattener { + public static flattenConnectionStatsReportObject( + statsReport: GroupCallStatsReport + ): Attributes { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report, + flatObject, + "matrix.stats.conn.", + 0 + ); + return flatObject; + } + + public static flattenByteSentStatsReportObject( + statsReport: GroupCallStatsReport + ): Attributes { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report, + flatObject, + "matrix.stats.bytesSent.", + 0 + ); + return flatObject; + } + + static flattenSummaryStatsReportObject( + statsReport: GroupCallStatsReport + ) { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report, + flatObject, + "matrix.stats.summary.", + 0 + ); + return flatObject; + } + + public static flattenObjectRecursive( + obj: Object, + flatObject: Attributes, + prefix: string, + depth: number + ): void { + if (depth > 10) + throw new Error( + "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + + prefix + ); + let entries; + if (obj instanceof Map) { + entries = obj.entries(); + } else { + entries = Object.entries(obj); + } + for (const [k, v] of entries) { + if (["string", "number", "boolean"].includes(typeof v) || v === null) { + let value; + value = v === null ? "null" : v; + value = typeof v === "number" && Number.isNaN(v) ? "NaN" : value; + flatObject[prefix + k] = value; + } else if (typeof v === "object") { + ObjectFlattener.flattenObjectRecursive( + v, + flatObject, + prefix + k + ".", + depth + 1 + ); + } + } + } +} diff --git a/src/otel/otel.ts b/src/otel/otel.ts index eac7ce4..d079da4 100644 --- a/src/otel/otel.ts +++ b/src/otel/otel.ts @@ -48,7 +48,7 @@ export class ElementCallOpenTelemetry { return sharedInstance; } - constructor(collectorUrl: string) { + constructor(collectorUrl: string | undefined) { const otlpExporter = new OTLPTraceExporter({ url: collectorUrl, }); diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 7a9f69a..48b8d8f 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -22,12 +22,19 @@ import { GroupCallErrorCode, GroupCallUnknownDeviceError, GroupCallError, + GroupCallStatsReportEvent, + GroupCallStatsReport, } from "matrix-js-sdk/src/webrtc/groupCall"; import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useTranslation } from "react-i18next"; import { IWidgetApiRequest } from "matrix-widget-api"; import { MatrixClient } from "matrix-js-sdk"; +import { + ByteSentStatsReport, + ConnectionStatsReport, + SummaryStatsReport, +} from "matrix-js-sdk/src/webrtc/stats/statsReport"; import { usePageUnload } from "./usePageUnload"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; @@ -337,6 +344,24 @@ export function useGroupCall( } } + function onConnectionStatsReport( + report: GroupCallStatsReport + ): void { + groupCallOTelMembership?.onConnectionStatsReport(report); + } + + function onByteSentStatsReport( + report: GroupCallStatsReport + ): void { + groupCallOTelMembership?.onByteSentStatsReport(report); + } + + function onSummaryStatsReport( + report: GroupCallStatsReport + ): void { + groupCallOTelMembership?.onSummaryStatsReport(report); + } + groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged); groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged); groupCall.on( @@ -353,6 +378,18 @@ export function useGroupCall( groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged); groupCall.on(GroupCallEvent.Error, onError); + groupCall.on( + GroupCallStatsReportEvent.ConnectionStats, + onConnectionStatsReport + ); + + groupCall.on( + GroupCallStatsReportEvent.ByteSentStats, + onByteSentStatsReport + ); + + groupCall.on(GroupCallStatsReportEvent.SummaryStats, onSummaryStatsReport); + updateState({ error: null, state: groupCall.state, @@ -399,6 +436,18 @@ export function useGroupCall( onParticipantsChanged ); groupCall.removeListener(GroupCallEvent.Error, onError); + groupCall.removeListener( + GroupCallStatsReportEvent.ConnectionStats, + onConnectionStatsReport + ); + groupCall.removeListener( + GroupCallStatsReportEvent.ByteSentStats, + onByteSentStatsReport + ); + groupCall.removeListener( + GroupCallStatsReportEvent.SummaryStats, + onSummaryStatsReport + ); leaveCall(); }; }, [groupCall, updateState, leaveCall]); diff --git a/test/otel/ObjectFlattener-test.ts b/test/otel/ObjectFlattener-test.ts new file mode 100644 index 0000000..a0258c0 --- /dev/null +++ b/test/otel/ObjectFlattener-test.ts @@ -0,0 +1,215 @@ +import { ObjectFlattener } from "../../src/otel/ObjectFlattener"; + +/* +Copyright 2023 New Vector Ltd + +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. +*/ +describe("ObjectFlattener", () => { + const statsReport = { + report: { + bandwidth: { upload: 426, download: 0 }, + bitrate: { + upload: 426, + download: 0, + audio: { + upload: 124, + download: 0, + }, + video: { + upload: 302, + download: 0, + }, + }, + packetLoss: { + total: 0, + download: 0, + upload: 0, + }, + framerate: { + local: new Map([ + ["LOCAL_AUDIO_TRACK_ID", 0], + ["LOCAL_VIDEO_TRACK_ID", 30], + ]), + remote: new Map([ + ["REMOTE_AUDIO_TRACK_ID", 0], + ["REMOTE_VIDEO_TRACK_ID", 60], + ]), + }, + resolution: { + local: new Map([ + ["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }], + ["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }], + ]), + remote: new Map([ + ["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }], + ["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }], + ]), + }, + codec: { + local: new Map([ + ["LOCAL_AUDIO_TRACK_ID", "opus"], + ["LOCAL_VIDEO_TRACK_ID", "v8"], + ]), + remote: new Map([ + ["REMOTE_AUDIO_TRACK_ID", "opus"], + ["REMOTE_VIDEO_TRACK_ID", "v9"], + ]), + }, + transport: [ + { + ip: "ff11::5fa:abcd:999c:c5c5:50000", + type: "udp", + localIp: "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", + isFocus: true, + localCandidateType: "host", + remoteCandidateType: "host", + networkType: "ethernet", + rtt: NaN, + }, + { + ip: "10.10.10.2:22222", + type: "tcp", + localIp: "10.10.10.100:33333", + isFocus: true, + localCandidateType: "srfx", + remoteCandidateType: "srfx", + networkType: "ethernet", + rtt: null, + }, + ], + }, + }; + describe("on flattenObjectRecursive", () => { + it("should flatter an Map object", () => { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report.resolution, + flatObject, + "matrix.stats.conn.resolution.", + 0 + ); + expect(flatObject).toEqual({ + "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.height": -1, + "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.width": -1, + + "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, + "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, + + "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": -1, + "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": -1, + + "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, + "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, + }); + }); + it("should flatter an Array object", () => { + const flatObject = {}; + ObjectFlattener.flattenObjectRecursive( + statsReport.report.transport, + flatObject, + "matrix.stats.conn.transport.", + 0 + ); + expect(flatObject).toEqual({ + "matrix.stats.conn.transport.0.ip": "ff11::5fa:abcd:999c:c5c5:50000", + "matrix.stats.conn.transport.0.type": "udp", + "matrix.stats.conn.transport.0.localIp": + "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", + "matrix.stats.conn.transport.0.isFocus": true, + "matrix.stats.conn.transport.0.localCandidateType": "host", + "matrix.stats.conn.transport.0.remoteCandidateType": "host", + "matrix.stats.conn.transport.0.networkType": "ethernet", + "matrix.stats.conn.transport.0.rtt": "NaN", + "matrix.stats.conn.transport.1.ip": "10.10.10.2:22222", + "matrix.stats.conn.transport.1.type": "tcp", + "matrix.stats.conn.transport.1.localIp": "10.10.10.100:33333", + "matrix.stats.conn.transport.1.isFocus": true, + "matrix.stats.conn.transport.1.localCandidateType": "srfx", + "matrix.stats.conn.transport.1.remoteCandidateType": "srfx", + "matrix.stats.conn.transport.1.networkType": "ethernet", + "matrix.stats.conn.transport.1.rtt": "null", + }); + }); + }); + + describe("on flattenConnectionStatsReportObject", () => { + it("should flatten a Report to otel Attributes Object", () => { + expect( + ObjectFlattener.flattenConnectionStatsReportObject(statsReport) + ).toEqual({ + "matrix.stats.conn.bandwidth.download": 0, + "matrix.stats.conn.bandwidth.upload": 426, + "matrix.stats.conn.bitrate.audio.download": 0, + "matrix.stats.conn.bitrate.audio.upload": 124, + "matrix.stats.conn.bitrate.download": 0, + "matrix.stats.conn.bitrate.upload": 426, + "matrix.stats.conn.bitrate.video.download": 0, + "matrix.stats.conn.bitrate.video.upload": 302, + "matrix.stats.conn.codec.local.LOCAL_AUDIO_TRACK_ID": "opus", + "matrix.stats.conn.codec.local.LOCAL_VIDEO_TRACK_ID": "v8", + "matrix.stats.conn.codec.remote.REMOTE_AUDIO_TRACK_ID": "opus", + "matrix.stats.conn.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9", + "matrix.stats.conn.framerate.local.LOCAL_AUDIO_TRACK_ID": 0, + "matrix.stats.conn.framerate.local.LOCAL_VIDEO_TRACK_ID": 30, + "matrix.stats.conn.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0, + "matrix.stats.conn.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60, + "matrix.stats.conn.packetLoss.download": 0, + "matrix.stats.conn.packetLoss.total": 0, + "matrix.stats.conn.packetLoss.upload": 0, + "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.height": -1, + "matrix.stats.conn.resolution.local.LOCAL_AUDIO_TRACK_ID.width": -1, + "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460, + "matrix.stats.conn.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780, + "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.height": -1, + "matrix.stats.conn.resolution.remote.REMOTE_AUDIO_TRACK_ID.width": -1, + "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960, + "matrix.stats.conn.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080, + "matrix.stats.conn.transport.0.ip": "ff11::5fa:abcd:999c:c5c5:50000", + "matrix.stats.conn.transport.0.type": "udp", + "matrix.stats.conn.transport.0.localIp": + "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000", + "matrix.stats.conn.transport.0.isFocus": true, + "matrix.stats.conn.transport.0.localCandidateType": "host", + "matrix.stats.conn.transport.0.remoteCandidateType": "host", + "matrix.stats.conn.transport.0.networkType": "ethernet", + "matrix.stats.conn.transport.0.rtt": "NaN", + "matrix.stats.conn.transport.1.ip": "10.10.10.2:22222", + "matrix.stats.conn.transport.1.type": "tcp", + "matrix.stats.conn.transport.1.localIp": "10.10.10.100:33333", + "matrix.stats.conn.transport.1.isFocus": true, + "matrix.stats.conn.transport.1.localCandidateType": "srfx", + "matrix.stats.conn.transport.1.remoteCandidateType": "srfx", + "matrix.stats.conn.transport.1.networkType": "ethernet", + "matrix.stats.conn.transport.1.rtt": "null", + }); + }); + }); + + describe("on flattenByteSendStatsReportObject", () => { + const byteSent = { + report: new Map([ + ["4aa92608-04c6-428e-8312-93e17602a959", 132093], + ["a08e4237-ee30-4015-a932-b676aec894b1", 913448], + ]), + }; + it("should flatten a Report to otel Attributes Object", () => { + expect( + ObjectFlattener.flattenByteSentStatsReportObject(byteSent) + ).toEqual({ + "matrix.stats.bytesSent.4aa92608-04c6-428e-8312-93e17602a959": 132093, + "matrix.stats.bytesSent.a08e4237-ee30-4015-a932-b676aec894b1": 913448, + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index e25096f..d98a26f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1822,9 +1822,9 @@ integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== "@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.5": - version "0.1.0-alpha.5" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz#60ede2c43b9d808ba8cf46085a3b347b290d9658" - integrity sha512-2KjAgWNGfuGLNjJwsrs6gGX157vmcTfNrA4u249utgnMPbJl7QwuUqh1bGxQ0PpK06yvZjgPlkna0lTbuwtuQw== + version "0.1.0-alpha.6" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.6.tgz#c0bdb9ab0d30179b8ef744d1b4010b0ad0ab9c3a" + integrity sha512-7hMffzw7KijxDyyH/eUyTfrLeCQHuyU3kaPOKGhcl3DZ3vx7bCncqjGMGTnxNPoP23I6gosvKSbO+3wYOT24Xg== "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz": version "3.2.14" @@ -5726,7 +5726,12 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@^1.0.4, content-type@~1.0.4: +content-type@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== @@ -10413,9 +10418,9 @@ log-symbols@^4.1.0: is-unicode-supported "^0.1.0" loglevel@^1.7.1: - version "1.8.0" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" - integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== + version "1.8.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" + integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== long@^2.4.0: version "2.4.0" @@ -10545,9 +10550,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#fe79a6fa7ca50fc7d078e11826b5539bb0822c45": version "24.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/042f2ed76c501c10dde98a31732fd92d862e2187" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fe79a6fa7ca50fc7d078e11826b5539bb0822c45" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" @@ -10570,14 +10575,6 @@ matrix-widget-api@^1.3.1: "@types/events" "^3.0.0" events "^3.2.0" -matrix-widget-api@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz#e38f404c76bb15c113909505c1c1a5b4d781c2f5" - integrity sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q== - dependencies: - "@types/events" "^3.0.0" - events "^3.2.0" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" From 2435846f66a8abf9a9e7273334865f86107abf04 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 5 Apr 2023 10:00:16 +0100 Subject: [PATCH 39/43] Latest js-sdk develop (with required PR merged) --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 86a666f..a8e5bfc 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "i18next-browser-languagedetector": "^6.1.8", "i18next-http-backend": "^1.4.4", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#fe79a6fa7ca50fc7d078e11826b5539bb0822c45", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#e89467c9fbf98182def2088a947155f30fdc7d1f", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/yarn.lock b/yarn.lock index d98a26f..84a1c95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10550,9 +10550,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#fe79a6fa7ca50fc7d078e11826b5539bb0822c45": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#e89467c9fbf98182def2088a947155f30fdc7d1f": version "24.0.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fe79a6fa7ca50fc7d078e11826b5539bb0822c45" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e89467c9fbf98182def2088a947155f30fdc7d1f" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5" From b061cbfb2f528943dd0b0a85e92b4ea05f20ea54 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 5 Apr 2023 10:01:58 +0100 Subject: [PATCH 40/43] Remove the other listeners --- src/otel/OTelCall.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/otel/OTelCall.ts b/src/otel/OTelCall.ts index 3717abf..79cc38d 100644 --- a/src/otel/OTelCall.ts +++ b/src/otel/OTelCall.ts @@ -41,10 +41,26 @@ export class OTelCall { } public dispose() { + this.call.peerConn.removeEventListener( + "connectionstatechange", + this.onCallConnectionStateChanged + ); + this.call.peerConn.removeEventListener( + "signalingstatechange", + this.onCallSignalingStateChanged + ); this.call.peerConn.removeEventListener( "iceconnectionstatechange", this.onIceConnectionStateChanged ); + this.call.peerConn.removeEventListener( + "icegatheringstatechange", + this.onIceGatheringStateChanged + ); + this.call.peerConn.removeEventListener( + "icecandidateerror", + this.onIceCandidateError + ); } private addCallPeerConnListeners = (): void => { From 0dcaa90650f8096a727fe80d861b194350f2a6d2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 5 Apr 2023 13:06:55 +0100 Subject: [PATCH 41/43] Fix exception when loading PostHog PostHog was expecting the matrix client object to be initialised at the point it ran its setup, which wasn't the case. Check to see if it's there on login and add an onLoginStatusChanged hook that to re-check. Also make a few methods private that didn't need to be public. Also fix a few instances where the OpenTelemetry group call tried to report metrics using a tracer which didn't exist anymore, if the user disabled analytics and then joined the same call again. --- src/ClientContext.tsx | 2 ++ src/analytics/PosthogAnalytics.ts | 17 +++++++++++++---- src/otel/OTelGroupCallMembership.ts | 10 +++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 46dfe5a..6a7d175 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -342,6 +342,8 @@ export const ClientProvider: FC = ({ children }) => { useEffect(() => { window.matrixclient = client; window.isPasswordlessUser = isPasswordlessUser; + + PosthogAnalytics.instance.onLoginStatusChanged(); }, [client, isPasswordlessUser]); if (error) { diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index e2e8fda..0f8ba38 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -227,7 +227,7 @@ export class PosthogAnalytics { .join(""); } - public async identifyUser(analyticsIdGenerator: () => string) { + private async identifyUser(analyticsIdGenerator: () => string) { if (this.anonymity == Anonymity.Pseudonymous && this.enabled) { // Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows // different devices to send the same ID. @@ -319,7 +319,12 @@ export class PosthogAnalytics { this.setAnonymity(Anonymity.Disabled); } - public updateSuperProperties() { + public onLoginStatusChanged(): void { + const optInAnalytics = getSetting("opt-in-analytics", false); + this.updateAnonymityAndIdentifyUser(optInAnalytics); + } + + private updateSuperProperties() { // Update super properties in posthog with our platform (app version, platform). // These properties will be subsequently passed in every event. // @@ -339,7 +344,7 @@ export class PosthogAnalytics { return this.eventSignup.getSignupEndTime() > new Date(0); } - public async updateAnonymityAndIdentifyUser( + private async updateAnonymityAndIdentifyUser( pseudonymousOptIn: boolean ): Promise { // Update this.anonymity based on the user's analytics opt-in settings @@ -348,6 +353,10 @@ export class PosthogAnalytics { : Anonymity.Disabled; this.setAnonymity(anonymity); + // We may not yet have a Matrix client at this point, if not, bail. This should get + // triggered again by onLoginStatusChanged once we do have a client. + if (!window.matrixclient) return; + if (anonymity === Anonymity.Pseudonymous) { this.setRegistrationType( window.matrixclient.isGuest() || window.isPasswordlessUser @@ -385,7 +394,7 @@ export class PosthogAnalytics { this.capture(eventName, properties, options); } - public startListeningToSettingsChanges(): void { + private startListeningToSettingsChanges(): void { // Listen to account data changes from sync so we can observe changes to relevant flags and update. // This is called - // * On page load, when the account data is first received by sync diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 8aa24c7..14c0f98 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -124,6 +124,8 @@ export class OTelGroupCallMembership { } public onJoinCall() { + if (!ElementCallOpenTelemetry.instance) return; + // Create the main span that tracks the time we intend to be in the call this.callMembershipSpan = ElementCallOpenTelemetry.instance.tracer.startSpan( @@ -174,7 +176,7 @@ export class OTelGroupCallMembership { for (const [userId, userCalls] of calls.entries()) { for (const [deviceId, call] of userCalls.entries()) { if (!this.callsByCallId.has(call.callId)) { - const span = ElementCallOpenTelemetry.instance.tracer.startSpan( + const span = ElementCallOpenTelemetry.instance?.tracer.startSpan( `matrix.call`, undefined, this.groupCallContext @@ -321,6 +323,8 @@ export class OTelGroupCallMembership { public onConnectionStatsReport( statsReport: GroupCallStatsReport ) { + if (!ElementCallOpenTelemetry.instance) return; + const type = OTelStatsReportType.ConnectionReport; const data = ObjectFlattener.flattenConnectionStatsReportObject(statsReport); @@ -330,6 +334,8 @@ export class OTelGroupCallMembership { public onByteSentStatsReport( statsReport: GroupCallStatsReport ) { + if (!ElementCallOpenTelemetry.instance) return; + const type = OTelStatsReportType.ByteSentReport; const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport); this.buildStatsEventSpan({ type, data }); @@ -338,6 +344,8 @@ export class OTelGroupCallMembership { public onSummaryStatsReport( statsReport: GroupCallStatsReport ) { + if (!ElementCallOpenTelemetry.instance) return; + const type = OTelStatsReportType.SummaryReport; const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport); this.buildStatsEventSpan({ type, data }); From 5e4aa53997b0edc9034c331fdf81b0463eee1410 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 5 Apr 2023 15:00:14 +0100 Subject: [PATCH 42/43] Don't call posthog before its initialised --- src/ClientContext.tsx | 3 ++- src/analytics/PosthogAnalytics.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 6a7d175..08593a4 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -343,7 +343,8 @@ export const ClientProvider: FC = ({ children }) => { window.matrixclient = client; window.isPasswordlessUser = isPasswordlessUser; - PosthogAnalytics.instance.onLoginStatusChanged(); + if (PosthogAnalytics.hasInstance()) + PosthogAnalytics.instance.onLoginStatusChanged(); }, [client, isPasswordlessUser]); if (error) { diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 0f8ba38..ed8ada3 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -102,6 +102,10 @@ export class PosthogAnalytics { private platformSuperProperties = {}; private registrationType: RegistrationType = RegistrationType.Guest; + public static hasInstance(): boolean { + return Boolean(this.internalInstance); + } + public static get instance(): PosthogAnalytics { if (!this.internalInstance) { this.internalInstance = new PosthogAnalytics(posthog); From fec299ab20ae381ecba05da0e7aa9dfb402910bb Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 5 Apr 2023 15:11:51 +0100 Subject: [PATCH 43/43] Skip whole block if no otel instance --- src/otel/OTelGroupCallMembership.ts | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts index 14c0f98..c8fce61 100644 --- a/src/otel/OTelGroupCallMembership.ts +++ b/src/otel/OTelGroupCallMembership.ts @@ -176,21 +176,23 @@ export class OTelGroupCallMembership { for (const [userId, userCalls] of calls.entries()) { for (const [deviceId, call] of userCalls.entries()) { if (!this.callsByCallId.has(call.callId)) { - const span = ElementCallOpenTelemetry.instance?.tracer.startSpan( - `matrix.call`, - undefined, - this.groupCallContext - ); - // XXX: anonymity - span.setAttribute("matrix.call.target.userId", userId); - span.setAttribute("matrix.call.target.deviceId", deviceId); - const displayName = - this.groupCall.room.getMember(userId)?.name ?? "unknown"; - span.setAttribute("matrix.call.target.displayName", displayName); - this.callsByCallId.set( - call.callId, - new OTelCall(userId, deviceId, call, span) - ); + if (ElementCallOpenTelemetry.instance) { + const span = ElementCallOpenTelemetry.instance.tracer.startSpan( + `matrix.call`, + undefined, + this.groupCallContext + ); + // XXX: anonymity + span.setAttribute("matrix.call.target.userId", userId); + span.setAttribute("matrix.call.target.deviceId", deviceId); + const displayName = + this.groupCall.room.getMember(userId)?.name ?? "unknown"; + span.setAttribute("matrix.call.target.displayName", displayName); + this.callsByCallId.set( + call.callId, + new OTelCall(userId, deviceId, call, span) + ); + } } } }