diff --git a/config/otel_dev/README.md b/config/otel_dev/README.md new file mode 100644 index 0000000..ea6c09a --- /dev/null +++ b/config/otel_dev/README.md @@ -0,0 +1,18 @@ +# 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 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. + +This also adds an nginx to add CORS headers to the jaeger query endpoint, such that it can +be used from webapps like stalk (https://deniz.co/stalk/). The CORS enabled endpoint is +exposed on port 16687. To use stalk, you should simply be able to navigate to it and add +http://127.0.0.1:16687/api as a data source. + +(Yes, we could enable the OTLP collector in jaeger all-in-one and passed this through +the nginx to enable CORS too, rather than running a separate collector. There's no reason +it's done this way other than that I'd already set up the separate collector.) + +Running `docker compose up` in this directory should be all you need. diff --git a/config/otel_dev/collector-gateway.yaml b/config/otel_dev/collector-gateway.yaml new file mode 100644 index 0000000..9c1a9cd --- /dev/null +++ b/config/otel_dev/collector-gateway.yaml @@ -0,0 +1,41 @@ +receivers: + otlp: + protocols: + http: + 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 '*' + #- "https://pr976--element-call.netlify.app" + - "https://*" + allowed_headers: + - "*" +processors: + batch: + timeout: 1s + resource: + attributes: + - key: test.key + value: "test-value" + action: insert +exporters: + logging: + loglevel: info + jaeger: + endpoint: jaeger-all-in-one:14250 + tls: + insecure: true +extensions: + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 +service: + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch, resource] + exporters: [logging, jaeger] diff --git a/config/otel_dev/docker-compose.yaml b/config/otel_dev/docker-compose.yaml new file mode 100644 index 0000000..0b43abe --- /dev/null +++ b/config/otel_dev/docker-compose.yaml @@ -0,0 +1,29 @@ +version: "2" +services: + # Jaeger + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268" + - "14250" + # Collector + collector-gateway: + image: otel/opentelemetry-collector:latest + volumes: + - ./collector-gateway.yaml:/etc/collector-gateway.yaml + command: ["--config=/etc/collector-gateway.yaml"] + ports: + - "1888:1888" # pprof extension + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + - "55670:55679" # zpages extension + depends_on: + - jaeger-all-in-one + nginx: + image: nginxinc/nginx-unprivileged:latest + volumes: + - ./nginx_otel.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "16687:8080" diff --git a/config/otel_dev/nginx_otel.conf b/config/otel_dev/nginx_otel.conf new file mode 100644 index 0000000..90d6970 --- /dev/null +++ b/config/otel_dev/nginx_otel.conf @@ -0,0 +1,16 @@ +server { + listen 8080; + server_name localhost; + + location / { + proxy_pass http://jaeger-all-in-one:16686/; + add_header Access-Control-Allow-Origin *; + + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Origin *; + add_header Content-Type text/plain; + add_header Content-Length 0; + return 204; + } + } +} diff --git a/package.json b/package.json index efa077d..f7fc0e1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,13 @@ "dependencies": { "@juggle/resize-observer": "^3.3.1", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", + "@opentelemetry/api": "^1.4.0", + "@opentelemetry/context-zone": "^1.9.1", + "@opentelemetry/exporter-jaeger": "^1.9.1", + "@opentelemetry/exporter-trace-otlp-http": "^0.35.1", + "@opentelemetry/instrumentation-document-load": "^0.31.1", + "@opentelemetry/instrumentation-user-interaction": "^0.32.1", + "@opentelemetry/sdk-trace-web": "^1.9.1", "@react-aria/button": "^3.3.4", "@react-aria/dialog": "^3.1.4", "@react-aria/focus": "^3.5.0", @@ -46,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#da03c3b529576a8fcde6f2c9a171fa6cca012830", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#042f2ed76c501c10dde98a31732fd92d862e2187", "matrix-widget-api": "^1.3.1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts new file mode 100644 index 0000000..8f9ba26 --- /dev/null +++ b/src/analytics/OtelPosthogExporter.ts @@ -0,0 +1,73 @@ +/* +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 { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base"; +import { ExportResult, ExportResultCode } from "@opentelemetry/core"; + +import { PosthogAnalytics } from "./PosthogAnalytics"; + +/** + * This is implementation of {@link SpanExporter} that sends spans + * to Posthog + */ +export class PosthogSpanExporter implements SpanExporter { + /** + * Export spans. + * @param spans + * @param resultCallback + */ + async export( + 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, + } + ); + resultCallback({ code: ExportResultCode.SUCCESS }); + } + } + /** + * Shutdown the exporter. + */ + shutdown(): Promise { + console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter"); + return new Promise((resolve, _reject) => { + resolve(); + }); + } +} diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index e2e8fda..718a49c 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -385,6 +385,22 @@ 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 - diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 5899d45..8b0fae1 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -36,6 +36,14 @@ export interface ConfigOptions { submit_url: string; }; + /** + * Sets the URL to send opentelemetry data to. If unset, opentelemetry will + * be disabled. + */ + opentelemetry?: { + collector_url: string; + }; + // Describes the default homeserver to use. The same format as Element Web // (without identity servers as we don't use them). default_server_config?: { diff --git a/src/initializer.tsx b/src/initializer.tsx index e10a4ec..37e659e 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -23,6 +23,7 @@ import * as Sentry from "@sentry/react"; import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; +import { ElementCallOpenTelemetry } from "./otel/otel"; enum LoadState { None, @@ -35,6 +36,7 @@ class DependencyLoadStates { // olm: LoadState = LoadState.None; config: LoadState = LoadState.None; sentry: LoadState = LoadState.None; + openTelemetry: LoadState = LoadState.None; allDepsAreLoaded() { return !Object.values(this).some((s) => s !== LoadState.Loaded); @@ -209,6 +211,15 @@ export class Initializer { this.loadStates.sentry = LoadState.Loaded; } + // OpenTelemetry (also only after config loaded) + if ( + this.loadStates.openTelemetry === LoadState.None && + this.loadStates.config === LoadState.Loaded + ) { + ElementCallOpenTelemetry.globalInit(); + this.loadStates.openTelemetry = LoadState.Loaded; + } + if (this.loadStates.allDepsAreLoaded()) { // resolve if there is no dependency that is not loaded resolve(); diff --git a/src/otel/OTelGroupCallMembership.ts b/src/otel/OTelGroupCallMembership.ts new file mode 100644 index 0000000..53551b0 --- /dev/null +++ b/src/otel/OTelGroupCallMembership.ts @@ -0,0 +1,301 @@ +/* +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 opentelemetry, { Span, Attributes, Context } from "@opentelemetry/api"; +import { + GroupCall, + MatrixClient, + MatrixEvent, + RoomMember, +} 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"; + +import { ElementCallOpenTelemetry } from "./otel"; + +/** + * Flattens out an object into a single layer with components + * of the key separated by dots + */ +function flattenVoipEvent(event: VoipEvent): Attributes { + const flatObject = {}; + + flattenVoipEventRecursive( + event as unknown as Record, // XXX Types + flatObject, + "matrix.event.", + 0 + ); + + return flatObject; +} + +function flattenVoipEventRecursive( + obj: Record, + flatObject: Record, + prefix: string, + depth: number +) { + if (depth > 10) + throw new Error( + "Depth limit exceeded: aborting VoipEvent recursion. Prefix is " + prefix + ); + + for (const [k, v] of Object.entries(obj)) { + if (["string", "number"].includes(typeof v)) { + flatObject[prefix + k] = v; + } else if (typeof v === "object") { + flattenVoipEventRecursive( + v as Record, + flatObject, + prefix + k + ".", + depth + 1 + ); + } + } +} + +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()); + + this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged); + } + + dispose() { + this.groupCall.removeListener( + GroupCallEvent.CallsChanged, + this.onCallsChanged + ); + } + + public onJoinCall() { + // Create the main span that tracks the time we intend to be in the call + this.callMembershipSpan = + ElementCallOpenTelemetry.instance.tracer.startSpan( + "matrix.groupCallMembership" + ); + this.callMembershipSpan.setAttribute( + "matrix.confId", + 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 + ); + + this.groupCallContext = opentelemetry.trace.setSpan( + opentelemetry.context.active(), + this.callMembershipSpan + ); + + this.callMembershipSpan?.addEvent("matrix.joinCall"); + } + + public onLeaveCall() { + this.callMembershipSpan?.addEvent("matrix.leaveCall"); + + // and end the main span to indicate we've left + if (this.callMembershipSpan) this.callMembershipSpan.end(); + } + + public onUpdateRoomState(event: MatrixEvent) { + if ( + !event || + (!event.getType().startsWith("m.call") && + !event.getType().startsWith("org.matrix.msc3401.call")) + ) { + return; + } + + this.callMembershipSpan?.addEvent( + `matrix.roomStateEvent_${event.getType()}`, + flattenVoipEvent(event.getContent()) + ); + } + + 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; + + if (event.type === "toDevice") { + this.callMembershipSpan?.addEvent( + `matrix.sendToDeviceEvent_${event.eventType}`, + flattenVoipEvent(event) + ); + } else if (event.type === "sendEvent") { + this.callMembershipSpan?.addEvent( + `matrix.sendToRoomEvent_${event.eventType}`, + flattenVoipEvent(event) + ); + } + } + + 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, + }); + } + + public onSetMicrophoneMuted(setMuted: boolean) { + this.callMembershipSpan?.addEvent("matrix.setMicMuted", { + "matrix.microphone.muted": setMuted, + }); + } + + public onToggleLocalVideoMuted(newValue: boolean) { + this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", { + "matrix.video.muted": newValue, + }); + } + + public onSetLocalVideoMuted(setMuted: boolean) { + this.callMembershipSpan?.addEvent("matrix.setVidMuted", { + "matrix.video.muted": setMuted, + }); + } + + public onToggleScreensharing(newValue: boolean) { + this.callMembershipSpan?.addEvent("matrix.setVidMuted", { + "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/otel/otel.ts b/src/otel/otel.ts new file mode 100644 index 0000000..eac7ce4 --- /dev/null +++ b/src/otel/otel.ts @@ -0,0 +1,104 @@ +/* +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 { + ConsoleSpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; +import opentelemetry, { Tracer } from "@opentelemetry/api"; +import { Resource } from "@opentelemetry/resources"; +import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; +import { logger } from "@sentry/utils"; + +import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter"; +import { Anonymity } from "../analytics/PosthogAnalytics"; +import { Config } from "../config/Config"; +import { getSetting, settingsBus } from "../settings/useSetting"; + +const SERVICE_NAME = "element-call"; + +let sharedInstance: ElementCallOpenTelemetry; + +export class ElementCallOpenTelemetry { + private _provider: WebTracerProvider; + private _tracer: Tracer; + private _anonymity: Anonymity; + + static globalInit(): void { + settingsBus.on("opt-in-analytics", recheckOTelEnabledStatus); + recheckOTelEnabledStatus(getSetting("opt-in-analytics", false)); + } + + static get instance(): ElementCallOpenTelemetry { + return sharedInstance; + } + + constructor(collectorUrl: string) { + const otlpExporter = new OTLPTraceExporter({ + url: collectorUrl, + }); + const consoleExporter = new ConsoleSpanExporter(); + const posthogExporter = new PosthogSpanExporter(); + + // 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, + }), + }; + this._provider = new WebTracerProvider(providerConfig); + + this._provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); + this._provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter)); + this._provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); + opentelemetry.trace.setGlobalTracerProvider(this._provider); + + this._tracer = opentelemetry.trace.getTracer( + // This is not the serviceName shown in jaeger + "my-element-call-otl-tracer" + ); + } + + public get tracer(): Tracer { + return this._tracer; + } + + public get provider(): WebTracerProvider { + return this._provider; + } + + public get anonymity(): Anonymity { + return this._anonymity; + } +} + +function recheckOTelEnabledStatus(optInAnalayticsEnabled: boolean): void { + 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 (!shouldEnable && sharedInstance) { + logger.info("Stopping OpenTelemetry debug reporting"); + sharedInstance = undefined; + } +} diff --git a/src/room/GroupCallInspector.tsx b/src/room/GroupCallInspector.tsx index 399f8d7..ad67c7a 100644 --- a/src/room/GroupCallInspector.tsx +++ b/src/room/GroupCallInspector.tsx @@ -28,14 +28,25 @@ 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, VoipEvent } from "matrix-js-sdk/src/webrtc/call"; +import { + CallEvent, + CallState, + CallError, + MatrixCall, + VoipEvent, +} from "matrix-js-sdk/src/webrtc/call"; import styles from "./GroupCallInspector.module.css"; import { SelectInput } from "../input/SelectInput"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; interface InspectorContextState { eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] }; @@ -353,7 +364,7 @@ function reducer( function useGroupCallState( client: MatrixClient, groupCall: GroupCall, - showPollCallStats: boolean + otelGroupCallMembership: OTelGroupCallMembership ): InspectorContextState { const [state, dispatch] = useReducer(reducer, { localUserId: client.getUserId(), @@ -381,28 +392,55 @@ function useGroupCallState( callStateEvent, memberStateEvents, }); + + otelGroupCallMembership?.onUpdateRoomState(event); } function onReceivedVoipEvent(event: MatrixEvent) { dispatch({ type: ClientEvent.ReceivedVoipEvent, event }); + + otelGroupCallMembership?.onReceivedVoipEvent(event); } - function onSendVoipEvent(event: VoipEvent) { + function onSendVoipEvent(event: VoipEvent, call: MatrixCall) { dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event }); + + otelGroupCallMembership?.onSendEvent(call, event); + } + + function onCallStateChange( + newState: CallState, + _: CallState, + call: MatrixCall + ) { + 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("calls_changed", onCallsChanged); 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); @@ -412,8 +450,10 @@ function useGroupCallState( return () => { client.removeListener(RoomStateEvent.Events, onUpdateRoomState); - //groupCall.removeListener("calls_changed", onCallsChanged); 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); @@ -422,7 +462,7 @@ function useGroupCallState( onUndecryptableToDevice ); }; - }, [client, groupCall]); + }, [client, groupCall, otelGroupCallMembership]); return state; } @@ -430,17 +470,19 @@ function useGroupCallState( interface GroupCallInspectorProps { client: MatrixClient; groupCall: GroupCall; + otelGroupCallMembership: OTelGroupCallMembership; show: boolean; } export function GroupCallInspector({ client, groupCall, + otelGroupCallMembership, show, }: GroupCallInspectorProps) { const [currentTab, setCurrentTab] = useState("sequence-diagrams"); const [selectedUserId, setSelectedUserId] = useState(); - const state = useGroupCallState(client, groupCall, show); + const state = useGroupCallState(client, groupCall, otelGroupCallMembership); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, setState] = useContext(InspectorContext); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 7af31d6..ad011ef 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -81,7 +81,8 @@ export function GroupCallView({ screenshareFeeds, participants, unencryptedEventsFromUsers, - } = useGroupCall(groupCall); + otelGroupCallMembership, + } = useGroupCall(groupCall, client); const { t } = useTranslation(); const { setAudioInput, setVideoInput } = useMediaHandler(); @@ -143,7 +144,6 @@ export function GroupCallView({ ]); await groupCall.enter(); - PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date()); PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId); @@ -238,6 +238,7 @@ export function GroupCallView({ onLeave={onLeave} isEmbedded={isEmbedded} hideHeader={hideHeader} + otelGroupCallMembership={otelGroupCallMembership} /> ); } else { @@ -262,6 +263,7 @@ export function GroupCallView({ roomIdOrAlias={roomIdOrAlias} unencryptedEventsFromUsers={unencryptedEventsFromUsers} hideHeader={hideHeader} + otelGroupCallMembership={otelGroupCallMembership} /> ); } diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 1ed1be3..2e0e932 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -73,6 +73,7 @@ import { TileDescriptor } from "../video-grid/TileDescriptor"; import { AudioSink } from "../video-grid/AudioSink"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { NewVideoGrid } from "../video-grid/NewVideoGrid"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -100,6 +101,7 @@ interface Props { roomIdOrAlias: string; unencryptedEventsFromUsers: Set; hideHeader: boolean; + otelGroupCallMembership: OTelGroupCallMembership; } export function InCallView({ @@ -122,6 +124,7 @@ export function InCallView({ roomIdOrAlias, unencryptedEventsFromUsers, hideHeader, + otelGroupCallMembership, }: Props) { const { t } = useTranslation(); usePreventScroll(); @@ -440,6 +443,7 @@ export function InCallView({ {rageshakeRequestModalState.isOpen && ( diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index a25f337..2781640 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -44,6 +44,7 @@ import { GroupCallInspector } from "./GroupCallInspector"; import { OverflowMenu } from "./OverflowMenu"; import { Size } from "../Avatar"; import { ParticipantInfo } from "./useGroupCall"; +import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; function getPromptText( networkWaiting: boolean, @@ -106,6 +107,7 @@ interface Props { onLeave: () => void; isEmbedded: boolean; hideHeader: boolean; + otelGroupCallMembership: OTelGroupCallMembership; } export const PTTCallView: React.FC = ({ @@ -119,6 +121,7 @@ export const PTTCallView: React.FC = ({ onLeave, isEmbedded, hideHeader, + otelGroupCallMembership, }) => { const { t } = useTranslation(); const { modalState: inviteModalState, modalProps: inviteModalProps } = @@ -192,6 +195,7 @@ export const PTTCallView: React.FC = ({ >; hasLocalParticipant: boolean; unencryptedEventsFromUsers: Set; + otelGroupCallMembership: OTelGroupCallMembership; } interface State { @@ -84,6 +88,13 @@ interface State { hasLocalParticipant: boolean; } +// This is a bit of a hack, but we keep the opentelemetry tracker object at the file +// level so that it doesn't pop in & out of existence as react mounts & unmounts +// components. The right solution is probably for this to live in the js-sdk and have +// the same lifetime as groupcalls themselves. +let groupCallOTelMembership: OTelGroupCallMembership; +let groupCallOTelMembershipGroupCallId: string; + function getParticipants( groupCall: GroupCall ): Map> { @@ -124,7 +135,10 @@ function getParticipants( return participants; } -export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { +export function useGroupCall( + groupCall: GroupCall, + client: MatrixClient +): UseGroupCallReturnType { const [ { state, @@ -158,6 +172,19 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { hasLocalParticipant: false, }); + 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) { + groupCallOTelMembership = new OTelGroupCallMembership(groupCall, client); + groupCallOTelMembershipGroupCallId = groupCall.groupCallId; + } else { + groupCallOTelMembership = undefined; + } + } + const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer( (state: Set, newVal: string) => { return new Set(state).add(newVal); @@ -175,6 +202,11 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { [] ); + 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. @@ -367,12 +399,12 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { onParticipantsChanged ); groupCall.removeListener(GroupCallEvent.Error, onError); - groupCall.leave(); + leaveCall(); }; - }, [groupCall, updateState]); + }, [groupCall, updateState, leaveCall]); usePageUnload(() => { - groupCall.leave(); + leaveCall(); }); const initLocalCallFeed = useCallback( @@ -391,17 +423,21 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { 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 }); }); }, [groupCall, updateState]); - const leave = useCallback(() => groupCall.leave(), [groupCall]); - const toggleLocalVideoMuted = useCallback(() => { const toggleToMute = !groupCall.isLocalVideoMuted(); groupCall.setLocalVideoMuted(toggleToMute); + groupCallOTelMembership?.onToggleLocalVideoMuted(toggleToMute); + // TODO: These explict posthog calls should be unnecessary now with the posthog otel exporter? PosthogAnalytics.instance.eventMuteCamera.track( toggleToMute, groupCall.groupCallId @@ -411,6 +447,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { const setMicrophoneMuted = useCallback( (setMuted) => { groupCall.setMicrophoneMuted(setMuted); + groupCallOTelMembership?.onSetMicrophoneMuted(setMuted); PosthogAnalytics.instance.eventMuteMicrophone.track( setMuted, groupCall.groupCallId @@ -421,10 +458,13 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { const toggleMicrophoneMuted = useCallback(() => { const toggleToMute = !groupCall.isMicrophoneMuted(); + groupCallOTelMembership?.onToggleMicrophoneMuted(toggleToMute); setMicrophoneMuted(toggleToMute); }, [groupCall, setMicrophoneMuted]); const toggleScreensharing = useCallback(async () => { + groupCallOTelMembership?.onToggleScreensharing(!groupCall.isScreensharing); + if (!groupCall.isScreensharing()) { // toggling on updateState({ requestingScreenshare: true }); @@ -525,7 +565,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { error, initLocalCallFeed, enter, - leave, + leave: leaveCall, toggleLocalVideoMuted, toggleMicrophoneMuted, toggleScreensharing, @@ -537,5 +577,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType { participants, hasLocalParticipant, unencryptedEventsFromUsers, + otelGroupCallMembership: groupCallOTelMembership, }; } diff --git a/yarn.lock b/yarn.lock index 539ebf9..e25096f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1910,6 +1910,138 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@opentelemetry/api@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.0.tgz#2c91791a9ba6ca0a0f4aaac5e45d58df13639ac8" + integrity sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g== + +"@opentelemetry/context-zone-peer-dep@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-zone-peer-dep/-/context-zone-peer-dep-1.9.1.tgz#634b1a25eebc68484d3568865ee5a2321b6b020d" + integrity sha512-4qaNi2noNMlT3DhOzXN4qKDiyZFjowj2vnfdtcAHZUwpIP0MQlpE3JYCr+2w7zKGJDfEOp2hg2A9Dkn8TqvzSw== + +"@opentelemetry/context-zone@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-zone/-/context-zone-1.9.1.tgz#1f1c48fb491283ab32320b3d95e542a3a3e86035" + integrity sha512-Kx2n9ftRokgHUAI6CIxsNepCsEP/fggDBH3GT27GdZkqgPYZqBn+nlTS23dB6etjWcSRd0piJnT3OIEnaxyIGA== + dependencies: + "@opentelemetry/context-zone-peer-dep" "1.9.1" + zone.js "^0.11.0" + +"@opentelemetry/core@1.9.1", "@opentelemetry/core@^1.8.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.9.1.tgz#e343337e1a7bf30e9a6aef3ef659b9b76379762a" + integrity sha512-6/qon6tw2I8ZaJnHAQUUn4BqhTbTNRS0WP8/bA0ynaX+Uzp/DDbd0NS0Cq6TMlh8+mrlsyqDE7mO50nmv2Yvlg== + dependencies: + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/exporter-jaeger@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-jaeger/-/exporter-jaeger-1.9.1.tgz#941d39c2d425021c734354bbc280a4ae19f95aad" + integrity sha512-6F10NMOtBT3HdxpT0IwYf1BX8RzZB7SpqHTvZsB2vzUvxVAyoLX8+cuo6Ke9sHS9YMqoTA3rER5x9kC6NOxEMQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + jaeger-client "^3.15.0" + +"@opentelemetry/exporter-trace-otlp-http@^0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.35.1.tgz#9bf988f91fb145b29a051bce8ff5ef85029ca575" + integrity sha512-EJgAsrvscKsqb/GzF1zS74vM+Z/aQRhrFE7hs/1GK1M9bLixaVyMGwg2pxz1wdYdjxS1mqpHMhXU+VvMvFCw1w== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/otlp-exporter-base" "0.35.1" + "@opentelemetry/otlp-transformer" "0.35.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + +"@opentelemetry/instrumentation-document-load@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-document-load/-/instrumentation-document-load-0.31.1.tgz#a535a5d1d71706701d3ff560a700b9dd03e4fb59" + integrity sha512-Ej4EB3m7GXzj4o8zF73amcnqXroN6/QdURjDAOgxN27zvvurR84larzGD7PjqgzzdtV+T7e/0BK07M0I2eA8PQ== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.35.1" + "@opentelemetry/sdk-trace-base" "^1.0.0" + "@opentelemetry/sdk-trace-web" "^1.8.0" + "@opentelemetry/semantic-conventions" "^1.0.0" + +"@opentelemetry/instrumentation-user-interaction@^0.32.1": + version "0.32.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-user-interaction/-/instrumentation-user-interaction-0.32.1.tgz#654c0352c2f7d5bb6cc21f07f9ec56f18f2cc854" + integrity sha512-27we7cENzEtO2oCRiEkYG4cFe1v94ybeLvM+5jqNDkZF7UY0GlctCW+jvqf569Z3Gs7yHrakO2sZf4EMEfTFWg== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.35.1" + "@opentelemetry/sdk-trace-web" "^1.8.0" + +"@opentelemetry/instrumentation@^0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.35.1.tgz#065bdbc4771137347e648eb4c6c6de6e9e97e4d1" + integrity sha512-EZsvXqxenbRTSNsft6LDcrT4pjAiyZOx3rkDNeqKpwZZe6GmZtsXaZZKuDkJtz9fTjOGjDHjZj9/h80Ya9iIJw== + dependencies: + require-in-the-middle "^5.0.3" + semver "^7.3.2" + shimmer "^1.2.1" + +"@opentelemetry/otlp-exporter-base@0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.35.1.tgz#535166608d5d36e6c959b2857d01245ee3a916b1" + integrity sha512-Sc0buJIs8CfUeQCL/12vDDjBREgsqHdjboBa/kPQDgMf008OBJSM02Ijj6T1TH+QVHl/VHBBEVJF+FTf0EH9Vg== + dependencies: + "@opentelemetry/core" "1.9.1" + +"@opentelemetry/otlp-transformer@0.35.1": + version "0.35.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.35.1.tgz#d4333b71324b83dbb1b0b3a4cfd769b3e214c6f9" + integrity sha512-c0HXcJ49MKoWSaA49J8PXlVx48CeEFpL0odP6KBkVT+Bw6kAe8JlI3mIezyN05VCDJGtS2I5E6WEsE+DJL1t9A== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/sdk-metrics" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + +"@opentelemetry/resources@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.9.1.tgz#5ad3d80ba968a3a0e56498ce4bc82a6a01f2682f" + integrity sha512-VqBGbnAfubI+l+yrtYxeLyOoL358JK57btPMJDd3TCOV3mV5TNBmzvOfmesM4NeTyXuGJByd3XvOHvFezLn3rQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/sdk-metrics@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.9.1.tgz#babc162a81df9884c16b1e67c2dd26ab478f3080" + integrity sha512-AyhKDcA8NuV7o1+9KvzRMxNbATJ8AcrutKilJ6hWSo9R5utnzxgffV4y+Hp4mJn84iXxkv+CBb99GOJ2A5OMzA== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + lodash.merge "4.6.2" + +"@opentelemetry/sdk-trace-base@1.9.1", "@opentelemetry/sdk-trace-base@^1.0.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.9.1.tgz#c349491b432a7e0ae7316f0b48b2d454d79d2b84" + integrity sha512-Y9gC5M1efhDLYHeeo2MWcDDMmR40z6QpqcWnPCm4Dmh+RHAMf4dnEBBntIe1dDpor686kyU6JV1D29ih1lZpsQ== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/resources" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/sdk-trace-web@^1.8.0", "@opentelemetry/sdk-trace-web@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-web/-/sdk-trace-web-1.9.1.tgz#9734c62dfb554336779c0eb4f78bb622d8bde988" + integrity sha512-VCnr8IYW1GYonGF8M3nDqUGFjf2jcL3nlhnNyF3PKGw6OI7xNCBR+65IgW5Va7QhDP0D01jRVJ9oNuTshrVewA== + dependencies: + "@opentelemetry/core" "1.9.1" + "@opentelemetry/sdk-trace-base" "1.9.1" + "@opentelemetry/semantic-conventions" "1.9.1" + +"@opentelemetry/semantic-conventions@1.9.1", "@opentelemetry/semantic-conventions@^1.0.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.9.1.tgz#ad3367684a57879392513479e0a436cb2ac46dad" + integrity sha512-oPQdbFDmZvjXk5ZDoBGXG8B4tSB/qW5vQunJWQMFUBp7Xe8O1ByPANueJ+Jzg58esEBegyyxZ7LRmfJr7kFcFg== + "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.7" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz#58f8217ba70069cc6a73f5d7e05e85b458c150e2" @@ -4164,6 +4296,11 @@ ansi-align@^3.0.0: dependencies: string-width "^4.1.0" +ansi-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a" + integrity sha512-bF6xLaZBLpOQzgYUtYEhJx090nPSZk1BQ/q2oyBK9aMMcJHzx9uXGCjI2Y+LebsN4Jwoykr0V9whbPiogdyHoQ== + ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" @@ -4985,6 +5122,16 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bufrw@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.3.0.tgz#28d6cfdaf34300376836310f5c31d57eeb40c8fa" + integrity sha512-jzQnSbdJqhIltU9O5KUiTtljP9ccw2u5ix59McQy4pV2xGhVLhRZIndY8GIrgh5HjXa6+QJ9AQhOd2QWQizJFQ== + dependencies: + ansi-color "^0.2.1" + error "^7.0.0" + hexer "^1.5.0" + xtend "^4.0.0" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -5132,15 +5279,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001359: - version "1.0.30001363" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15" - integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg== - -caniuse-lite@^1.0.30001400: - version "1.0.30001425" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001425.tgz#52917791a453eb3265143d2cd08d80629e82c735" - integrity sha512-/pzFv0OmNG6W0ym80P3NtapU0QEiDS3VuYAZMGoLLqiC7f6FJFe1MjpQDREGApeenD9wloeytmVDj+JLXPC6qw== +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001359, caniuse-lite@^1.0.30001400: + version "1.0.30001460" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz" + integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== case-sensitive-paths-webpack-plugin@^2.3.0: version "2.4.0" @@ -6901,6 +7043,21 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.3.4" +error@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" + integrity sha512-UtVv4l5MhijsYUxPJo4390gzfZvAnTHreNnDjnTZaKIiZ/SemXxAhBkYSKtWa5RtBXbLP8tMgn/n0RUa/H7jXw== + dependencies: + string-template "~0.2.1" + xtend "~4.0.0" + +error@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/error/-/error-7.2.1.tgz#eab21a4689b5f684fc83da84a0e390de82d94894" + integrity sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA== + dependencies: + string-template "~0.2.1" + es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" @@ -8581,6 +8738,16 @@ heimdalljs@^0.2.6: dependencies: rsvp "~3.2.1" +hexer@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653" + integrity sha512-dyrPC8KzBzUJ19QTIo1gXNqIISRXQ0NwteW6OeQHRN4ZuZeHkdODfj0zHBdOlHbRY8GqbqK57C9oWSvQZizFsg== + dependencies: + ansi-color "^0.2.1" + minimist "^1.1.0" + process "^0.10.0" + xtend "^4.0.0" + highlight.js@^10.4.1, highlight.js@~10.7.0: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" @@ -9468,6 +9635,17 @@ iterate-value@^1.0.2: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" +jaeger-client@^3.15.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/jaeger-client/-/jaeger-client-3.19.0.tgz#9b5bd818ebd24e818616ee0f5cffe1722a53ae6e" + integrity sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw== + dependencies: + node-int64 "^0.4.0" + opentracing "^0.14.4" + thriftrw "^3.5.0" + uuid "^8.3.2" + xorshift "^1.1.1" + jest-changed-files@^29.2.0: version "29.2.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289" @@ -10211,7 +10389,7 @@ lodash.flow@^3.3.0: resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" integrity sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw== -lodash.merge@^4.6.2: +lodash.merge@4.6.2, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== @@ -10239,6 +10417,11 @@ loglevel@^1.7.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== +long@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f" + integrity sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -10362,9 +10545,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#da03c3b529576a8fcde6f2c9a171fa6cca012830": +"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/da03c3b529576a8fcde6f2c9a171fa6cca012830" + 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.5" @@ -10387,6 +10570,14 @@ 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" @@ -10619,6 +10810,11 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimist@^1.1.0: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -10701,6 +10897,11 @@ mktemp@~0.4.0: resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" integrity sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A== +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== + moment-mini@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.24.0.tgz#fa68d98f7fe93ae65bf1262f6abb5fb6983d8d18" @@ -11083,6 +11284,11 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +opentracing@^0.14.4: + version "0.14.7" + resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5" + integrity sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -11940,6 +12146,11 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/process/-/process-0.10.1.tgz#842457cc51cfed72dc775afeeafb8c6034372725" + integrity sha512-dyIett8dgGIZ/TXKUzeYExt7WA6ldDzys9vTDU/cCA9L17Ypme+KzS+NjQCjpn9xsvi/shbMC+yP/BcFMBz0NA== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -12670,6 +12881,15 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-in-the-middle@^5.0.3: + version "5.2.0" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz#4b71e3cc7f59977100af9beb76bf2d056a5a6de2" + integrity sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg== + dependencies: + debug "^4.1.1" + module-details-from-path "^1.0.3" + resolve "^1.22.1" + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -12714,7 +12934,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1, resolve@^1.3.2: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -13067,6 +13287,11 @@ shelljs@0.8.4: interpret "^1.0.0" rechoir "^0.6.2" +shimmer@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" + integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== + side-channel@^1.0.3, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -13356,6 +13581,11 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== + "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -13677,6 +13907,15 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thriftrw@^3.5.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/thriftrw/-/thriftrw-3.12.0.tgz#30857847755e7f036b2e0a79d11c9f55075539d9" + integrity sha512-4YZvR4DPEI41n4Opwr4jmrLGG4hndxr7387kzRFIIzxHQjarPusH4lGXrugvgb7TtPrfZVTpZCVe44/xUxowEw== + dependencies: + bufrw "^1.3.0" + error "7.0.2" + long "^2.4.0" + through2-filter@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" @@ -13866,6 +14105,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" + integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -14315,6 +14559,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -14833,6 +15082,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xorshift@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/xorshift/-/xorshift-1.2.0.tgz#30a4cdd8e9f8d09d959ed2a88c42a09c660e8148" + integrity sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g== + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -14904,6 +15158,13 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zone.js@^0.11.0: + version "0.11.8" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.8.tgz#40dea9adc1ad007b5effb2bfed17f350f1f46a21" + integrity sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA== + dependencies: + tslib "^2.3.0" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"