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 fe52c85f4..389eeb70c 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 000000000..feceef761 --- /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 46727f5f9..000000000 --- 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 beea4c868..fd98a62af 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 ae43b5bc5..a96bd53bb 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 a6278c2ec..06a6720c9 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 bad173ea4..49c272bfb 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 6339e42d9..c31637b6a 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 f4e460d7d..fe9186178 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 9b4951b49..1e44dad76 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 90a1cb5f2..288083156 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 756ac74bd..0fe5fe24c 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 bad7a0e61..188747cce 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 60f2d67da..d6c785a1b 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 3b6368dab..f7e2263c3 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 3b6c84f7a..cd1d7efdc 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 f7e2263c3..8a95ba776 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 648a0a1fc..399f8d722 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 cd1d7efdc..1852bfea5 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 6cc85a04f..52f90c37a 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 288083156..6e38b5fad 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 0fe5fe24c..13288b645 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 52f90c37a..7b1fad001 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 13288b645..f2a2bfcdb 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 a32fd8c94..3f31e74e2 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 764249f34..c11b6e012 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 000000000..d45360cd5 --- /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 0b54c82fb..1c691816a 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 000000000..a0258c011 --- /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 8a95ba776..efa077d45 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 1852bfea5..539ebf912 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 eee140642..ba27e267a 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 f49980d2c..35587973d 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 a32fd8c94..4ecc42dab 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 764249f34..4c6d308b0 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 25de3acf9..5d3e7d34b 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 c058297a5..ad838fb8d 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 0b54c82fb..553d4186d 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 ddb3fc680..58b450a4b 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 4c6d308b0..432970b93 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 7b1fad001..9c1a9cd0a 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 5d3e7d34b..eac7ce46e 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 392445f4e..b5916298f 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 432970b93..53551b036 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 ad838fb8d..ad67c7aba 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 9c1a9cd0a..f9e3b90f8 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 f9e3b90f8..9c1a9cd0a 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 b5916298f..7a9f69ab1 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 8fe102d00..ea6c09a23 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 4fbfa909f..8f9ba26bf 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 cfaaf771f..8b0fae185 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 9c1a9cd0a..f9e3b90f8 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 f7fc0e19d..2bec81e32 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 268520968..2c9a01633 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 d45360cd5..c332aba09 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 030219b90..52fc31bbd 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 e25096f83..66c1dc993 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 f9e3b90f8..9c1a9cd0a 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 2bec81e32..2e4fa35c9 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 66c1dc993..595cae5a8 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 2e4fa35c9..86a666f7a 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 2c9a01633..78f171f03 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 c332aba09..dcda0783b 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 52fc31bbd..48b8d8f19 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 595cae5a8..d98a26fee 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 78f171f03..45d5bfdfe 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 eac7ce46e..d079da41f 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 53551b036..2ec11bd8f 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 53551b036..3c739161a 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 53551b036..95112a1eb 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 9c1a9cd0a..f9e3b90f8 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 2ec11bd8f..69bf024f7 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 8f9ba26bf..c624a0078 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 718a49c2d..e2e8fdaed 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 3c739161a..bd116f4dc 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 000000000..3717abfd0 --- /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 45d5bfdfe..6d3d3b758 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 f7fc0e19d..86a666f7a 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 b9b03ad46..49c9354db 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 000000000..dcda0783b --- /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 eac7ce46e..d079da41f 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 7a9f69ab1..48b8d8f19 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 000000000..a0258c011 --- /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 e25096f83..d98a26fee 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 86a666f7a..a8e5bfc5c 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 d98a26fee..84a1c952d 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 3717abfd0..79cc38d57 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 46dfe5a5c..6a7d17538 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 e2e8fdaed..0f8ba38d2 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 8aa24c747..14c0f98d1 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 6a7d17538..08593a4b1 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 0f8ba38d2..ed8ada35f 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 14c0f98d1..c8fce617e 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) + ); + } } } }