Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30a224e20e | ||
|
|
3c7f01a510 | ||
|
|
2b5de6db03 | ||
|
|
8eafb1ae4a | ||
|
|
3da4b4eeef | ||
|
|
c31185ffef | ||
|
|
0de1aa74ee | ||
|
|
838137c83b | ||
|
|
f627835646 | ||
|
|
9442b314b2 | ||
|
|
7221b7c3a2 | ||
|
|
370a6579fb | ||
|
|
5bec960112 | ||
|
|
da7760d7ab | ||
|
|
a17ffcc327 | ||
|
|
d211d27817 | ||
|
|
0637804d61 | ||
|
|
a2b3e098b6 | ||
|
|
4bcddad316 | ||
|
|
e2293665f9 | ||
|
|
95eca18207 | ||
|
|
2f33902ea9 | ||
|
|
6999765f39 | ||
|
|
480e46c5b2 | ||
|
|
bb5c382fd0 | ||
|
|
2b71a6c4f4 | ||
|
|
dd1485a277 | ||
|
|
caea22fa89 | ||
|
|
858c68baf1 | ||
|
|
de3bad3810 | ||
|
|
88f3b30040 | ||
|
|
928f1c1d6f | ||
|
|
711cdf9a60 | ||
|
|
b2317dac84 | ||
|
|
fec299ab20 | ||
|
|
5e4aa53997 | ||
|
|
0dcaa90650 | ||
|
|
7b88c4330e | ||
|
|
b061cbfb2f | ||
|
|
2435846f66 | ||
|
|
23ddd73f4f | ||
|
|
390442a4c3 | ||
|
|
c824ea6f9a | ||
|
|
28196a2e9d | ||
|
|
5b70def4d2 | ||
|
|
2cd549cdc8 | ||
|
|
e0089a0aee | ||
|
|
61a0534984 | ||
|
|
29223b62ad | ||
|
|
a52251befa | ||
|
|
30f75c6cd2 | ||
|
|
8fa23b7da9 | ||
|
|
277081ee2a | ||
|
|
3a7983d2de | ||
|
|
3b06258e40 | ||
|
|
c53dbfde27 | ||
|
|
889a31489b | ||
|
|
cb0ba6d827 | ||
|
|
e18c69ec89 | ||
|
|
47e0ca2eda | ||
|
|
e870188be3 | ||
|
|
dd67a45671 | ||
|
|
707272bf19 | ||
|
|
dc725f90a9 | ||
|
|
a1aca7bdf2 | ||
|
|
773f2e009d | ||
|
|
5e6c33b3b5 | ||
|
|
72403d1aea | ||
|
|
74b218af8c | ||
|
|
c2b78d59c6 | ||
|
|
21458c8840 | ||
|
|
f96ce8985d | ||
|
|
848e28ef92 | ||
|
|
4bf1fbfd8e | ||
|
|
34a72679a1 | ||
|
|
77c6357b08 | ||
|
|
66c3d05ae9 | ||
|
|
c4f029ae4f | ||
|
|
8978f94fe4 | ||
|
|
40f5c53c05 | ||
|
|
5f41f9476b | ||
|
|
d1ba5dff38 | ||
|
|
313ebe258e | ||
|
|
48493a96e1 | ||
|
|
ec88907981 | ||
|
|
9c0adfd32e | ||
|
|
f6fb65be49 | ||
|
|
3d6ae3fbc3 | ||
|
|
359e055314 | ||
|
|
6696af9b3f | ||
|
|
9b02d17224 | ||
|
|
6b36604c84 | ||
|
|
ef9934ce6b | ||
|
|
e7a7cf3eb8 | ||
|
|
63ede0b51a | ||
|
|
2d91b43a7d | ||
|
|
f8f5d2011d | ||
|
|
521b0a857a | ||
|
|
31450219c8 | ||
|
|
22d2404370 | ||
|
|
c519e13885 | ||
|
|
1e2cd97764 | ||
|
|
0cca5ae174 | ||
|
|
971eca59ff | ||
|
|
4c59638d00 |
18
config/otel_dev/README.md
Normal file
18
config/otel_dev/README.md
Normal file
@@ -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.
|
||||
41
config/otel_dev/collector-gateway.yaml
Normal file
41
config/otel_dev/collector-gateway.yaml
Normal file
@@ -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"
|
||||
- "http://*"
|
||||
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]
|
||||
29
config/otel_dev/docker-compose.yaml
Normal file
29
config/otel_dev/docker-compose.yaml
Normal file
@@ -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"
|
||||
16
config/otel_dev/nginx_otel.conf
Normal file
16
config/otel_dev/nginx_otel.conf
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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#90234402a71955d60ca75a068e5450bdafed0b41",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"mermaid": "^8.13.8",
|
||||
"normalize.css": "^8.0.1",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"{{name}} is talking…": "{{name}} is talking…",
|
||||
"{{names}}, {{name}}": "{{names}}, {{name}}",
|
||||
"{{roomName}} - Walkie-talkie call": "{{roomName}} - Walkie-talkie call",
|
||||
"<0></0><1></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></0><1></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?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>": "<0>Already have an account?</0><1><0>Log in</0> Or <2>Access as a guest</2></1>",
|
||||
"<0>Create an account</0> Or <2>Access as a guest</2>": "<0>Create an account</0> Or <2>Access as a guest</2>",
|
||||
"<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>": "<0>Join call now</0><1>Or</1><2>Copy call link and join later</2>",
|
||||
@@ -21,7 +22,7 @@
|
||||
"Avatar": "Avatar",
|
||||
"By clicking \"Go\", you agree to our <2>Terms and conditions</2>": "By clicking \"Go\", you agree to our <2>Terms and conditions</2>",
|
||||
"By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>": "By clicking \"Join call now\", you agree to our <2>Terms and conditions</2>",
|
||||
"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</2> and our <5>Cookie Policy</5>.": "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</2> and our <5>Cookie Policy</5>.",
|
||||
"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",
|
||||
|
||||
15
scripts/reformat-release-notes.py
Executable file
15
scripts/reformat-release-notes.py
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This script can be used to reformat the release notes generated by
|
||||
# GitHub releases into a format slightly more appropriate for our
|
||||
# project (ie. we don't really need to mention the author of every PR)
|
||||
#
|
||||
# eg. in: * OpenTelemetry by @dbkr in https://github.com/vector-im/element-call/pull/961
|
||||
# out: * OpenTelemetry (https://github.com/vector-im/element-call/pull/961)
|
||||
|
||||
import sys
|
||||
import re
|
||||
|
||||
for line in sys.stdin:
|
||||
matches = re.match(r'^\* (.*) by (\S+) in (\S+)$', line.strip())
|
||||
print("* %s (%s)" % (matches[1], matches[3]))
|
||||
@@ -342,6 +342,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
useEffect(() => {
|
||||
window.matrixclient = client;
|
||||
window.isPasswordlessUser = isPasswordlessUser;
|
||||
|
||||
if (PosthogAnalytics.hasInstance())
|
||||
PosthogAnalytics.instance.onLoginStatusChanged();
|
||||
}, [client, isPasswordlessUser]);
|
||||
|
||||
if (error) {
|
||||
|
||||
14
src/analytics/AnalyticsNotice.tsx
Normal file
14
src/analytics/AnalyticsNotice.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React, { FC } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { Link } from "../typography/Typography";
|
||||
|
||||
export const AnalyticsNotice: FC = () => (
|
||||
<Trans>
|
||||
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{" "}
|
||||
<Link href="https://element.io/privacy">Privacy Policy</Link> and our{" "}
|
||||
<Link href="https://element.io/cookie-policy">Cookie Policy</Link>.
|
||||
</Trans>
|
||||
);
|
||||
@@ -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 "
|
||||
)}
|
||||
</>
|
||||
<Link color="primary" href="https://element.io/privacy">
|
||||
<>{t("Privacy Policy")}</>
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -227,7 +231,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 +323,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 +348,7 @@ export class PosthogAnalytics {
|
||||
return this.eventSignup.getSignupEndTime() > new Date(0);
|
||||
}
|
||||
|
||||
public async updateAnonymityAndIdentifyUser(
|
||||
private async updateAnonymityAndIdentifyUser(
|
||||
pseudonymousOptIn: boolean
|
||||
): Promise<void> {
|
||||
// Update this.anonymity based on the user's analytics opt-in settings
|
||||
@@ -348,6 +357,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 +398,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
|
||||
|
||||
150
src/analytics/PosthogSpanProcessor.ts
Normal file
150
src/analytics/PosthogSpanProcessor.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
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 {
|
||||
SpanProcessor,
|
||||
ReadableSpan,
|
||||
Span,
|
||||
} from "@opentelemetry/sdk-trace-base";
|
||||
import { hrTimeToMilliseconds } from "@opentelemetry/core";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
|
||||
interface PrevCall {
|
||||
callId: string;
|
||||
hangupTs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Span processor that extracts certain metrics from spans to send to PostHog
|
||||
*/
|
||||
export class PosthogSpanProcessor implements SpanProcessor {
|
||||
async forceFlush(): Promise<void> {}
|
||||
|
||||
onStart(span: Span): void {
|
||||
// Hack: Yield to allow attributes to be set before processing
|
||||
Promise.resolve().then(() => {
|
||||
switch (span.name) {
|
||||
case "matrix.groupCallMembership":
|
||||
this.onGroupCallMembershipStart(span);
|
||||
return;
|
||||
case "matrix.groupCallMembership.summaryReport":
|
||||
this.onSummaryReportStart(span);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onEnd(span: ReadableSpan): void {
|
||||
switch (span.name) {
|
||||
case "matrix.groupCallMembership":
|
||||
this.onGroupCallMembershipEnd(span);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private onGroupCallMembershipStart(span: ReadableSpan): void {
|
||||
const prevCall = this.prevCall;
|
||||
const newCallId = span.attributes["matrix.confId"] as string;
|
||||
|
||||
// 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 && newCallId === prevCall.callId) {
|
||||
const duration = hrTimeToMilliseconds(span.startTime) - prevCall.hangupTs;
|
||||
if (duration <= maxRejoinMs) {
|
||||
PosthogAnalytics.instance.trackEvent({
|
||||
eventName: "Rejoin",
|
||||
callId: prevCall.callId,
|
||||
rejoinDuration: duration,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onGroupCallMembershipEnd(span: ReadableSpan): void {
|
||||
this.prevCall = {
|
||||
callId: span.attributes["matrix.confId"] as string,
|
||||
hangupTs: hrTimeToMilliseconds(span.endTime),
|
||||
};
|
||||
}
|
||||
|
||||
private onSummaryReportStart(span: ReadableSpan): void {
|
||||
// Searching for an event like this:
|
||||
// matrix.stats.summary
|
||||
// matrix.stats.summary.percentageReceivedAudioMedia: 0.75
|
||||
// matrix.stats.summary.percentageReceivedMedia: 1
|
||||
// matrix.stats.summary.percentageReceivedVideoMedia: 0.75
|
||||
// matrix.stats.summary.maxJitter: 100
|
||||
// matrix.stats.summary.maxPacketLoss: 20
|
||||
const event = span.events.find((e) => e.name === "matrix.stats.summary");
|
||||
if (event !== undefined) {
|
||||
const attributes = event.attributes;
|
||||
if (attributes) {
|
||||
const mediaReceived = `${attributes["matrix.stats.summary.percentageReceivedMedia"]}`;
|
||||
const videoReceived = `${attributes["matrix.stats.summary.percentageReceivedVideoMedia"]}`;
|
||||
const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`;
|
||||
const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`;
|
||||
const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`;
|
||||
PosthogAnalytics.instance.trackEvent(
|
||||
{
|
||||
eventName: "MediaReceived",
|
||||
callId: span.attributes["matrix.confId"] as string,
|
||||
mediaReceived: mediaReceived,
|
||||
audioReceived: audioReceived,
|
||||
videoReceived: videoReceived,
|
||||
maxJitter: maxJitter,
|
||||
maxPacketLoss: maxPacketLoss,
|
||||
},
|
||||
// Send instantly because the window might be closing
|
||||
{ send_instantly: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the processor.
|
||||
*/
|
||||
shutdown(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
110
src/analytics/RageshakeSpanProcessor.ts
Normal file
110
src/analytics/RageshakeSpanProcessor.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Attributes } from "@opentelemetry/api";
|
||||
import { hrTimeToMicroseconds } from "@opentelemetry/core";
|
||||
import {
|
||||
SpanProcessor,
|
||||
ReadableSpan,
|
||||
Span,
|
||||
} from "@opentelemetry/sdk-trace-base";
|
||||
|
||||
const dumpAttributes = (attr: Attributes) =>
|
||||
Object.entries(attr).map(([key, value]) => ({
|
||||
key,
|
||||
type: typeof value,
|
||||
value,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Exports spans on demand to the Jaeger JSON format, which can be attached to
|
||||
* rageshakes and loaded into analysis tools like Jaeger and Stalk.
|
||||
*/
|
||||
export class RageshakeSpanProcessor implements SpanProcessor {
|
||||
private readonly spans: ReadableSpan[] = [];
|
||||
|
||||
async forceFlush(): Promise<void> {}
|
||||
|
||||
onStart(span: Span): void {
|
||||
this.spans.push(span);
|
||||
}
|
||||
|
||||
onEnd(): void {}
|
||||
|
||||
/**
|
||||
* Dumps the spans collected so far as Jaeger-compatible JSON.
|
||||
*/
|
||||
public dump(): string {
|
||||
const now = Date.now() * 1000; // Jaeger works in microseconds
|
||||
const traces = new Map<string, ReadableSpan[]>();
|
||||
|
||||
// Organize spans by their trace IDs
|
||||
for (const span of this.spans) {
|
||||
const traceId = span.spanContext().traceId;
|
||||
let trace = traces.get(traceId);
|
||||
|
||||
if (trace === undefined) {
|
||||
trace = [];
|
||||
traces.set(traceId, trace);
|
||||
}
|
||||
|
||||
trace.push(span);
|
||||
}
|
||||
|
||||
const processId = "p1";
|
||||
const processes = {
|
||||
[processId]: {
|
||||
serviceName: "element-call",
|
||||
tags: [],
|
||||
},
|
||||
warnings: null,
|
||||
};
|
||||
|
||||
return JSON.stringify({
|
||||
// Honestly not sure what some of these fields mean, I just know that
|
||||
// they're present in Jaeger JSON exports
|
||||
total: 0,
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
errors: null,
|
||||
data: [...traces.entries()].map(([traceId, spans]) => ({
|
||||
traceID: traceId,
|
||||
warnings: null,
|
||||
processes,
|
||||
spans: spans.map((span) => {
|
||||
const ctx = span.spanContext();
|
||||
const startTime = hrTimeToMicroseconds(span.startTime);
|
||||
// If the span has not yet ended, pretend that it ends now
|
||||
const duration =
|
||||
span.duration[0] === -1
|
||||
? now - startTime
|
||||
: hrTimeToMicroseconds(span.duration);
|
||||
|
||||
return {
|
||||
traceID: traceId,
|
||||
spanID: ctx.spanId,
|
||||
operationName: span.name,
|
||||
processID: processId,
|
||||
warnings: null,
|
||||
startTime,
|
||||
duration,
|
||||
references:
|
||||
span.parentSpanId === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
refType: "CHILD_OF",
|
||||
traceID: traceId,
|
||||
spanID: span.parentSpanId,
|
||||
},
|
||||
],
|
||||
tags: dumpAttributes(span.attributes),
|
||||
logs: span.events.map((event) => ({
|
||||
timestamp: hrTimeToMicroseconds(event.time),
|
||||
fields: dumpAttributes(event.attributes ?? {}),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {}
|
||||
}
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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<HTMLFormElement>;
|
||||
children: JSX.Element[];
|
||||
children: ReactNode[];
|
||||
}
|
||||
|
||||
export const Form = forwardRef<HTMLFormElement, FormProps>(
|
||||
|
||||
@@ -37,3 +37,7 @@ limitations under the License.
|
||||
.recentCallsTitle {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
color: var(--secondary-content);
|
||||
}
|
||||
|
||||
@@ -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<Error>();
|
||||
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")}
|
||||
</Button>
|
||||
</FieldRow>
|
||||
<InputField
|
||||
id="optInAnalytics"
|
||||
type="checkbox"
|
||||
checked={optInAnalytics}
|
||||
description={optInDescription()}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setOptInAnalytics(event.target.checked)
|
||||
}
|
||||
/>
|
||||
{optInAnalytics === null && (
|
||||
<Caption className={styles.notice}>
|
||||
<AnalyticsNotice />
|
||||
</Caption>
|
||||
)}
|
||||
{error && (
|
||||
<FieldRow className={styles.fieldRow}>
|
||||
<ErrorMessage error={error} />
|
||||
|
||||
@@ -45,3 +45,7 @@ limitations under the License.
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
color: var(--secondary-content);
|
||||
}
|
||||
|
||||
@@ -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<Error>();
|
||||
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"
|
||||
/>
|
||||
</FieldRow>
|
||||
<InputField
|
||||
id="optInAnalytics"
|
||||
type="checkbox"
|
||||
checked={optInAnalytics}
|
||||
description={optInDescription()}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setOptInAnalytics(event.target.checked)
|
||||
}
|
||||
/>
|
||||
<Caption>
|
||||
{optInAnalytics === null && (
|
||||
<Caption className={styles.notice}>
|
||||
<AnalyticsNotice />
|
||||
</Caption>
|
||||
)}
|
||||
<Caption className={styles.notice}>
|
||||
<Trans>
|
||||
By clicking "Go", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>Terms and conditions</Link>
|
||||
|
||||
@@ -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();
|
||||
|
||||
119
src/otel/OTelCall.ts
Normal file
119
src/otel/OTelCall.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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(
|
||||
"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 => {
|
||||
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);
|
||||
};
|
||||
}
|
||||
467
src/otel/OTelGroupCallMembership.ts
Normal file
467
src/otel/OTelGroupCallMembership.ts
Normal file
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
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,
|
||||
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";
|
||||
import { OTelCall } from "./OTelCall";
|
||||
|
||||
/**
|
||||
* 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<string, unknown>, // XXX Types
|
||||
flatObject,
|
||||
"matrix.event.",
|
||||
0
|
||||
);
|
||||
|
||||
return flatObject;
|
||||
}
|
||||
|
||||
function flattenVoipEventRecursive(
|
||||
obj: Record<string, unknown>,
|
||||
flatObject: Record<string, unknown>,
|
||||
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", "boolean"].includes(typeof v) || v === null) {
|
||||
flatObject[prefix + k] = v;
|
||||
} else if (typeof v === "object") {
|
||||
flattenVoipEventRecursive(
|
||||
v as Record<string, unknown>,
|
||||
flatObject,
|
||||
prefix + k + ".",
|
||||
depth + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = "unknown";
|
||||
private myDeviceId: string;
|
||||
private myMember?: RoomMember;
|
||||
private callsByCallId = new Map<string, OTelCall>();
|
||||
private statsReportSpan: {
|
||||
span: Span | undefined;
|
||||
stats: OTelStatsReportEvent[];
|
||||
};
|
||||
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
|
||||
|
||||
constructor(private groupCall: GroupCall, client: MatrixClient) {
|
||||
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);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.groupCall.removeListener(
|
||||
GroupCallEvent.CallsChanged,
|
||||
this.onCallsChanged
|
||||
);
|
||||
}
|
||||
|
||||
public onJoinCall() {
|
||||
if (!ElementCallOpenTelemetry.instance) return;
|
||||
if (this.callMembershipSpan !== undefined) {
|
||||
logger.warn("Call membership span is already started");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 ? this.myMember.name : "unknown-name"
|
||||
);
|
||||
|
||||
this.groupCallContext = opentelemetry.trace.setSpan(
|
||||
opentelemetry.context.active(),
|
||||
this.callMembershipSpan
|
||||
);
|
||||
|
||||
this.callMembershipSpan?.addEvent("matrix.joinCall");
|
||||
}
|
||||
|
||||
public onLeaveCall() {
|
||||
if (this.callMembershipSpan === undefined) {
|
||||
logger.warn("Call membership span is already ended");
|
||||
return;
|
||||
}
|
||||
|
||||
this.callMembershipSpan.addEvent("matrix.leaveCall");
|
||||
// and end the span to indicate we've left
|
||||
this.callMembershipSpan.end();
|
||||
this.callMembershipSpan = undefined;
|
||||
this.groupCallContext = undefined;
|
||||
}
|
||||
|
||||
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)) {
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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(
|
||||
`matrix.sendToDeviceEvent_${event.eventType}`,
|
||||
flattenVoipEvent(event)
|
||||
);
|
||||
} else if (event.type === "sendEvent") {
|
||||
callTrackingInfo.span.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 onSpeaking(member: RoomMember, deviceId: string, speaking: boolean) {
|
||||
if (speaking) {
|
||||
// Ensure that there's an audio activity span for this speaker
|
||||
let deviceMap = this.speakingSpans.get(member);
|
||||
if (deviceMap === undefined) {
|
||||
deviceMap = new Map();
|
||||
this.speakingSpans.set(member, deviceMap);
|
||||
}
|
||||
|
||||
if (!deviceMap.has(deviceId)) {
|
||||
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
|
||||
"matrix.audioActivity",
|
||||
undefined,
|
||||
this.groupCallContext
|
||||
);
|
||||
span.setAttribute("matrix.userId", member.userId);
|
||||
span.setAttribute("matrix.displayName", member.rawDisplayName);
|
||||
|
||||
deviceMap.set(deviceId, span);
|
||||
}
|
||||
} else {
|
||||
// End the audio activity span for this speaker, if any
|
||||
const deviceMap = this.speakingSpans.get(member);
|
||||
deviceMap?.get(deviceId)?.end();
|
||||
deviceMap?.delete(deviceId);
|
||||
|
||||
if (deviceMap?.size === 0) this.speakingSpans.delete(member);
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
public onConnectionStatsReport(
|
||||
statsReport: GroupCallStatsReport<ConnectionStatsReport>
|
||||
) {
|
||||
if (!ElementCallOpenTelemetry.instance) return;
|
||||
|
||||
const type = OTelStatsReportType.ConnectionReport;
|
||||
const data =
|
||||
ObjectFlattener.flattenConnectionStatsReportObject(statsReport);
|
||||
this.buildStatsEventSpan({ type, data });
|
||||
}
|
||||
|
||||
public onByteSentStatsReport(
|
||||
statsReport: GroupCallStatsReport<ByteSentStatsReport>
|
||||
) {
|
||||
if (!ElementCallOpenTelemetry.instance) return;
|
||||
|
||||
const type = OTelStatsReportType.ByteSentReport;
|
||||
const data = ObjectFlattener.flattenByteSentStatsReportObject(statsReport);
|
||||
this.buildStatsEventSpan({ type, data });
|
||||
}
|
||||
|
||||
public onSummaryStatsReport(
|
||||
statsReport: GroupCallStatsReport<SummaryStatsReport>
|
||||
) {
|
||||
if (!ElementCallOpenTelemetry.instance) return;
|
||||
|
||||
const type = OTelStatsReportType.SummaryReport;
|
||||
const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport);
|
||||
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
|
||||
const ctx = setSpan(
|
||||
opentelemetry.context.active(),
|
||||
this.callMembershipSpan
|
||||
);
|
||||
const span = ElementCallOpenTelemetry.instance?.tracer.startSpan(
|
||||
"matrix.groupCallMembership.summaryReport",
|
||||
undefined,
|
||||
ctx
|
||||
);
|
||||
if (span === undefined) {
|
||||
return;
|
||||
}
|
||||
span.setAttribute("matrix.confId", this.groupCall.groupCallId);
|
||||
span.setAttribute("matrix.userId", this.myUserId);
|
||||
span.setAttribute(
|
||||
"matrix.displayName",
|
||||
this.myMember ? this.myMember.name : "unknown-name"
|
||||
);
|
||||
span.addEvent(type, data);
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
if (this.statsReportSpan.span === undefined) {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
ConnectionReport = "matrix.stats.connection",
|
||||
ByteSentReport = "matrix.stats.byteSent",
|
||||
SummaryReport = "matrix.stats.summary",
|
||||
}
|
||||
97
src/otel/ObjectFlattener.ts
Normal file
97
src/otel/ObjectFlattener.ts
Normal file
@@ -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<ConnectionStatsReport>
|
||||
): Attributes {
|
||||
const flatObject = {};
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
statsReport.report,
|
||||
flatObject,
|
||||
"matrix.stats.conn.",
|
||||
0
|
||||
);
|
||||
return flatObject;
|
||||
}
|
||||
|
||||
public static flattenByteSentStatsReportObject(
|
||||
statsReport: GroupCallStatsReport<ByteSentStatsReport>
|
||||
): Attributes {
|
||||
const flatObject = {};
|
||||
ObjectFlattener.flattenObjectRecursive(
|
||||
statsReport.report,
|
||||
flatObject,
|
||||
"matrix.stats.bytesSent.",
|
||||
0
|
||||
);
|
||||
return flatObject;
|
||||
}
|
||||
|
||||
static flattenSummaryStatsReportObject(
|
||||
statsReport: GroupCallStatsReport<SummaryStatsReport>
|
||||
) {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/otel/otel.ts
Normal file
128
src/otel/otel.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
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 "matrix-js-sdk/src/logger";
|
||||
|
||||
import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor";
|
||||
import { Anonymity } from "../analytics/PosthogAnalytics";
|
||||
import { Config } from "../config/Config";
|
||||
import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor";
|
||||
|
||||
const SERVICE_NAME = "element-call";
|
||||
|
||||
let sharedInstance: ElementCallOpenTelemetry;
|
||||
|
||||
export class ElementCallOpenTelemetry {
|
||||
private _provider: WebTracerProvider;
|
||||
private _tracer: Tracer;
|
||||
private _anonymity: Anonymity;
|
||||
private otlpExporter: OTLPTraceExporter;
|
||||
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
|
||||
|
||||
static globalInit(): void {
|
||||
const config = Config.get();
|
||||
// we always enable opentelemetry in general. We only enable the OTLP
|
||||
// collector if a URL is defined (and in future if another setting is defined)
|
||||
// The posthog exporteer is always enabled, posthog reporting is enabled or disabled
|
||||
// within the posthog code.
|
||||
const shouldEnableOtlp = Boolean(config.opentelemetry?.collector_url);
|
||||
|
||||
if (!sharedInstance || sharedInstance.isOtlpEnabled !== shouldEnableOtlp) {
|
||||
logger.info("(Re)starting OpenTelemetry debug reporting");
|
||||
sharedInstance?.dispose();
|
||||
|
||||
sharedInstance = new ElementCallOpenTelemetry(
|
||||
config.opentelemetry?.collector_url,
|
||||
config.rageshake?.submit_url
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static get instance(): ElementCallOpenTelemetry {
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
constructor(
|
||||
collectorUrl: string | undefined,
|
||||
rageshakeUrl: string | undefined
|
||||
) {
|
||||
// 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);
|
||||
|
||||
if (collectorUrl) {
|
||||
logger.info("Enabling OTLP collector with URL " + collectorUrl);
|
||||
this.otlpExporter = new OTLPTraceExporter({
|
||||
url: collectorUrl,
|
||||
});
|
||||
this._provider.addSpanProcessor(
|
||||
new SimpleSpanProcessor(this.otlpExporter)
|
||||
);
|
||||
} else {
|
||||
logger.info("OTLP collector disabled");
|
||||
}
|
||||
|
||||
if (rageshakeUrl) {
|
||||
this.rageshakeProcessor = new RageshakeSpanProcessor();
|
||||
this._provider.addSpanProcessor(this.rageshakeProcessor);
|
||||
}
|
||||
|
||||
this._provider.addSpanProcessor(
|
||||
new SimpleSpanProcessor(new ConsoleSpanExporter())
|
||||
);
|
||||
this._provider.addSpanProcessor(new PosthogSpanProcessor());
|
||||
opentelemetry.trace.setGlobalTracerProvider(this._provider);
|
||||
|
||||
this._tracer = opentelemetry.trace.getTracer(
|
||||
// This is not the serviceName shown in jaeger
|
||||
"my-element-call-otl-tracer"
|
||||
);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
opentelemetry.trace.setGlobalTracerProvider(null);
|
||||
this._provider?.shutdown();
|
||||
}
|
||||
|
||||
public get isOtlpEnabled(): boolean {
|
||||
return Boolean(this.otlpExporter);
|
||||
}
|
||||
|
||||
public get tracer(): Tracer {
|
||||
return this._tracer;
|
||||
}
|
||||
|
||||
public get provider(): WebTracerProvider {
|
||||
return this._provider;
|
||||
}
|
||||
|
||||
public get anonymity(): Anonymity {
|
||||
return this._anonymity;
|
||||
}
|
||||
}
|
||||
@@ -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<string>();
|
||||
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);
|
||||
|
||||
@@ -81,7 +81,8 @@ export function GroupCallView({
|
||||
screenshareFeeds,
|
||||
participants,
|
||||
unencryptedEventsFromUsers,
|
||||
} = useGroupCall(groupCall);
|
||||
otelGroupCallMembership,
|
||||
} = useGroupCall(groupCall, client);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { setAudioInput, setVideoInput } = useMediaHandler();
|
||||
@@ -142,8 +143,7 @@ export function GroupCallView({
|
||||
groupCall.setLocalVideoMuted(videoInput === null),
|
||||
]);
|
||||
|
||||
await groupCall.enter();
|
||||
|
||||
await enter();
|
||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
||||
|
||||
@@ -158,17 +158,17 @@ export function GroupCallView({
|
||||
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
};
|
||||
}
|
||||
}, [groupCall, preload, setAudioInput, setVideoInput]);
|
||||
}, [groupCall, preload, setAudioInput, setVideoInput, enter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmbedded && !preload) {
|
||||
// In embedded mode, bypass the lobby and just enter the call straight away
|
||||
groupCall.enter();
|
||||
enter();
|
||||
|
||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
||||
}
|
||||
}, [groupCall, isEmbedded, preload]);
|
||||
}, [groupCall, isEmbedded, preload, enter]);
|
||||
|
||||
useSentryGroupCallHandler(groupCall);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string>;
|
||||
hideHeader: boolean;
|
||||
otelGroupCallMembership: OTelGroupCallMembership;
|
||||
}
|
||||
|
||||
export function InCallView({
|
||||
@@ -122,6 +124,7 @@ export function InCallView({
|
||||
roomIdOrAlias,
|
||||
unencryptedEventsFromUsers,
|
||||
hideHeader,
|
||||
otelGroupCallMembership,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
usePreventScroll();
|
||||
@@ -357,11 +360,11 @@ export function InCallView({
|
||||
const audioElements: JSX.Element[] = [];
|
||||
if (!spatialAudio || maximisedParticipant) {
|
||||
for (const item of items) {
|
||||
if (item.isLocal) continue; // We don't want to render own audio
|
||||
audioElements.push(
|
||||
<AudioSink
|
||||
tileDescriptor={item}
|
||||
audioOutput={audioOutput}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
key={item.id}
|
||||
/>
|
||||
);
|
||||
@@ -440,6 +443,7 @@ export function InCallView({
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
show={showInspector}
|
||||
/>
|
||||
{rageshakeRequestModalState.isOpen && (
|
||||
|
||||
@@ -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<Props> = ({
|
||||
@@ -119,6 +121,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||
onLeave,
|
||||
isEmbedded,
|
||||
hideHeader,
|
||||
otelGroupCallMembership,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||
@@ -192,6 +195,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
// Never shown in PTT mode, but must be present to collect call state
|
||||
// https://github.com/vector-im/element-call/issues/328
|
||||
show={false}
|
||||
|
||||
@@ -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
|
||||
|
||||
65
src/room/checkForParallelCalls.ts
Normal file
65
src/room/checkForParallelCalls.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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 { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
|
||||
function isObject(x: unknown): x is Record<string, unknown> {
|
||||
return typeof x === "object" && x !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the state of a room for multiple calls happening in parallel, sending
|
||||
* the details to PostHog if that is indeed what's happening. (This is unwanted
|
||||
* as it indicates a split-brain scenario.)
|
||||
*/
|
||||
export function checkForParallelCalls(state: RoomState): void {
|
||||
const now = Date.now();
|
||||
const participantsPerCall = new Map<string, number>();
|
||||
|
||||
// For each participant in each call, increment the participant count
|
||||
for (const e of state.getStateEvents(EventType.GroupCallMemberPrefix)) {
|
||||
const content = e.getContent<Record<string, unknown>>();
|
||||
const calls: unknown[] = Array.isArray(content["m.calls"])
|
||||
? content["m.calls"]
|
||||
: [];
|
||||
|
||||
for (const call of calls) {
|
||||
if (isObject(call) && typeof call["m.call_id"] === "string") {
|
||||
const devices: unknown[] = Array.isArray(call["m.devices"])
|
||||
? call["m.devices"]
|
||||
: [];
|
||||
|
||||
for (const device of devices) {
|
||||
if (isObject(device) && (device["expires_ts"] as number) > now) {
|
||||
const participantCount =
|
||||
participantsPerCall.get(call["m.call_id"]) ?? 0;
|
||||
participantsPerCall.set(call["m.call_id"], participantCount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (participantsPerCall.size > 1) {
|
||||
PosthogAnalytics.instance.trackEvent({
|
||||
eventName: "ParallelCalls",
|
||||
participantsPerCall: Object.fromEntries(participantsPerCall),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,16 +22,27 @@ 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, RoomStateEvent } 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";
|
||||
import { TranslatedError, translatedError } from "../TranslatedError";
|
||||
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
||||
import { checkForParallelCalls } from "./checkForParallelCalls";
|
||||
|
||||
export enum ConnectionState {
|
||||
EstablishingCall = "establishing call", // call hasn't been established yet
|
||||
@@ -66,6 +77,7 @@ export interface UseGroupCallReturnType {
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
hasLocalParticipant: boolean;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
otelGroupCallMembership: OTelGroupCallMembership;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -84,6 +96,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<RoomMember, Map<string, ParticipantInfo>> {
|
||||
@@ -124,7 +143,10 @@ function getParticipants(
|
||||
return participants;
|
||||
}
|
||||
|
||||
export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||
export function useGroupCall(
|
||||
groupCall: GroupCall,
|
||||
client: MatrixClient
|
||||
): UseGroupCallReturnType {
|
||||
const [
|
||||
{
|
||||
state,
|
||||
@@ -158,6 +180,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<string>, newVal: string) => {
|
||||
return new Set(state).add(newVal);
|
||||
@@ -175,6 +210,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.
|
||||
@@ -305,6 +345,24 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||
}
|
||||
}
|
||||
|
||||
function onConnectionStatsReport(
|
||||
report: GroupCallStatsReport<ConnectionStatsReport>
|
||||
): void {
|
||||
groupCallOTelMembership?.onConnectionStatsReport(report);
|
||||
}
|
||||
|
||||
function onByteSentStatsReport(
|
||||
report: GroupCallStatsReport<ByteSentStatsReport>
|
||||
): void {
|
||||
groupCallOTelMembership?.onByteSentStatsReport(report);
|
||||
}
|
||||
|
||||
function onSummaryStatsReport(
|
||||
report: GroupCallStatsReport<SummaryStatsReport>
|
||||
): void {
|
||||
groupCallOTelMembership?.onSummaryStatsReport(report);
|
||||
}
|
||||
|
||||
groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
|
||||
groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
|
||||
groupCall.on(
|
||||
@@ -320,6 +378,19 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||
groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged);
|
||||
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);
|
||||
groupCall.room.currentState.on(
|
||||
RoomStateEvent.Update,
|
||||
checkForParallelCalls
|
||||
);
|
||||
|
||||
updateState({
|
||||
error: null,
|
||||
@@ -367,12 +438,28 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||
onParticipantsChanged
|
||||
);
|
||||
groupCall.removeListener(GroupCallEvent.Error, onError);
|
||||
groupCall.leave();
|
||||
groupCall.removeListener(
|
||||
GroupCallStatsReportEvent.ConnectionStats,
|
||||
onConnectionStatsReport
|
||||
);
|
||||
groupCall.removeListener(
|
||||
GroupCallStatsReportEvent.ByteSentStats,
|
||||
onByteSentStatsReport
|
||||
);
|
||||
groupCall.removeListener(
|
||||
GroupCallStatsReportEvent.SummaryStats,
|
||||
onSummaryStatsReport
|
||||
);
|
||||
groupCall.room.currentState.off(
|
||||
RoomStateEvent.Update,
|
||||
checkForParallelCalls
|
||||
);
|
||||
leaveCall();
|
||||
};
|
||||
}, [groupCall, updateState]);
|
||||
}, [groupCall, updateState, leaveCall]);
|
||||
|
||||
usePageUnload(() => {
|
||||
groupCall.leave();
|
||||
leaveCall();
|
||||
});
|
||||
|
||||
const initLocalCallFeed = useCallback(
|
||||
@@ -391,17 +478,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 +502,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 +513,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 +620,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||
error,
|
||||
initLocalCallFeed,
|
||||
enter,
|
||||
leave,
|
||||
leave: leaveCall,
|
||||
toggleLocalVideoMuted,
|
||||
toggleMicrophoneMuted,
|
||||
toggleScreensharing,
|
||||
@@ -537,5 +632,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||
participants,
|
||||
hasLocalParticipant,
|
||||
unencryptedEventsFromUsers,
|
||||
otelGroupCallMembership: groupCallOTelMembership,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -32,15 +32,14 @@ import {
|
||||
useSpatialAudio,
|
||||
useShowInspector,
|
||||
useOptInAnalytics,
|
||||
canEnableSpatialAudio,
|
||||
useNewGrid,
|
||||
useDeveloperSettingsTab,
|
||||
} from "./useSetting";
|
||||
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 +70,17 @@ export const SettingsModal = (props: Props) => {
|
||||
|
||||
const downloadDebugLog = useDownloadDebugLog();
|
||||
|
||||
const optInDescription = (
|
||||
<Caption>
|
||||
<Trans>
|
||||
<AnalyticsNotice />
|
||||
<br />
|
||||
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.
|
||||
</Trans>
|
||||
</Caption>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("Settings")}
|
||||
@@ -122,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<HTMLInputElement>) =>
|
||||
setSpatialAudio(event.target.checked)
|
||||
setSpatialAudio!(event.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
@@ -187,7 +197,7 @@ export const SettingsModal = (props: Props) => {
|
||||
id="optInAnalytics"
|
||||
type="checkbox"
|
||||
checked={optInAnalytics}
|
||||
description={optInDescription()}
|
||||
description={optInDescription}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setOptInAnalytics(event.target.checked)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,14 @@ import { useClient } from "../ClientContext";
|
||||
import { InspectorContext } from "../room/GroupCallInspector";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { Config } from "../config/Config";
|
||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
||||
|
||||
const gzip = (text: string): Blob => {
|
||||
// encode as UTF-8
|
||||
const buf = new TextEncoder().encode(text);
|
||||
// compress
|
||||
return new Blob([pako.gzip(buf)]);
|
||||
};
|
||||
|
||||
interface RageShakeSubmitOptions {
|
||||
sendLogs: boolean;
|
||||
@@ -235,14 +243,15 @@ export function useSubmitRageshake(): {
|
||||
const logs = await getLogsForReport();
|
||||
|
||||
for (const entry of logs) {
|
||||
// encode as UTF-8
|
||||
let buf = new TextEncoder().encode(entry.lines);
|
||||
// compress
|
||||
buf = pako.gzip(buf);
|
||||
|
||||
body.append("compressed-log", new Blob([buf]), entry.id);
|
||||
body.append("compressed-log", gzip(entry.lines), entry.id);
|
||||
}
|
||||
|
||||
body.append(
|
||||
"file",
|
||||
gzip(ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump()),
|
||||
"traces.json"
|
||||
);
|
||||
|
||||
if (inspectorState) {
|
||||
body.append(
|
||||
"file",
|
||||
|
||||
@@ -17,6 +17,11 @@ limitations under the License.
|
||||
import { EventEmitter } from "events";
|
||||
import { useMemo, useState, useEffect, useCallback } from "react";
|
||||
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
|
||||
type Setting<T> = [T, (value: T) => void];
|
||||
type DisableableSetting<T> = [T, ((value: T) => void) | null];
|
||||
|
||||
// Bus to notify other useSetting consumers when a setting is changed
|
||||
export const settingsBus = new EventEmitter();
|
||||
|
||||
@@ -24,10 +29,7 @@ const getSettingKey = (name: string): string => {
|
||||
return `matrix-setting-${name}`;
|
||||
};
|
||||
// Like useState, but reads from and persists the value to localStorage
|
||||
const useSetting = <T>(
|
||||
name: string,
|
||||
defaultValue: T
|
||||
): [T, (value: T) => void] => {
|
||||
const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
|
||||
const key = useMemo(() => getSettingKey(name), [name]);
|
||||
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
@@ -65,7 +67,7 @@ export const setSetting = <T>(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,17 +81,27 @@ export const canEnableSpatialAudio = () => {
|
||||
return userAgent.includes("Firefox");
|
||||
};
|
||||
|
||||
export const useSpatialAudio = (): [boolean, (val: boolean) => void] => {
|
||||
export const useSpatialAudio = (): DisableableSetting<boolean> => {
|
||||
const settingVal = useSetting("spatial-audio", false);
|
||||
if (canEnableSpatialAudio()) return settingVal;
|
||||
|
||||
return [false, (_: boolean) => {}];
|
||||
return [false, null];
|
||||
};
|
||||
|
||||
export const useShowInspector = () => useSetting("show-inspector", false);
|
||||
export const useOptInAnalytics = () => useSetting("opt-in-analytics", false);
|
||||
|
||||
// null = undecided
|
||||
export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
|
||||
const settingVal = useSetting<boolean | null>("opt-in-analytics", null);
|
||||
if (PosthogAnalytics.instance.isEnabled()) return settingVal;
|
||||
|
||||
return [false, null];
|
||||
};
|
||||
|
||||
export const useKeyboardShortcuts = () =>
|
||||
useSetting("keyboard-shortcuts", true);
|
||||
|
||||
export const useNewGrid = () => useSetting("new-grid", false);
|
||||
|
||||
export const useDeveloperSettingsTab = () =>
|
||||
useSetting("developer-settings-tab", false);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||
import { TileDescriptor } from "./TileDescriptor";
|
||||
import { useCallFeed } from "./useCallFeed";
|
||||
import { useMediaStream } from "./useMediaStream";
|
||||
@@ -23,6 +24,7 @@ import { useMediaStream } from "./useMediaStream";
|
||||
interface Props {
|
||||
tileDescriptor: TileDescriptor;
|
||||
audioOutput: string;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
}
|
||||
|
||||
// Renders and <audio> element on the page playing the given stream
|
||||
@@ -30,8 +32,12 @@ interface Props {
|
||||
export const AudioSink: React.FC<Props> = ({
|
||||
tileDescriptor,
|
||||
audioOutput,
|
||||
otelGroupCallMembership,
|
||||
}: Props) => {
|
||||
const { localVolume, stream } = useCallFeed(tileDescriptor.callFeed);
|
||||
const { localVolume, stream } = useCallFeed(
|
||||
tileDescriptor.callFeed,
|
||||
otelGroupCallMembership
|
||||
);
|
||||
|
||||
const audioElementRef = useMediaStream(
|
||||
stream,
|
||||
|
||||
@@ -20,7 +20,8 @@ limitations under the License.
|
||||
top: 0;
|
||||
width: var(--tileWidth);
|
||||
height: var(--tileHeight);
|
||||
border-radius: 8px;
|
||||
--tileRadius: 8px;
|
||||
border-radius: var(--tileRadius);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -51,7 +52,7 @@ limitations under the License.
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
content: "";
|
||||
border-radius: 20px;
|
||||
border-radius: var(--tileRadius);
|
||||
box-shadow: inset 0 0 0 4px var(--accent) !important;
|
||||
}
|
||||
|
||||
@@ -174,6 +175,6 @@ limitations under the License.
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.videoTile {
|
||||
border-radius: 20px;
|
||||
--tileRadius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { useState, useEffect } from "react";
|
||||
import { CallFeed, CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
|
||||
|
||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||
|
||||
interface CallFeedState {
|
||||
callFeed: CallFeed | undefined;
|
||||
isLocal: boolean;
|
||||
@@ -46,40 +48,46 @@ function getCallFeedState(callFeed: CallFeed | undefined): CallFeedState {
|
||||
};
|
||||
}
|
||||
|
||||
export function useCallFeed(callFeed: CallFeed | undefined): CallFeedState {
|
||||
export function useCallFeed(
|
||||
callFeed: CallFeed | undefined,
|
||||
otelGroupCallMembership?: OTelGroupCallMembership
|
||||
): CallFeedState {
|
||||
const [state, setState] = useState<CallFeedState>(() =>
|
||||
getCallFeedState(callFeed)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function onSpeaking(speaking: boolean) {
|
||||
setState((prevState) => ({ ...prevState, speaking }));
|
||||
}
|
||||
|
||||
function onMuteStateChanged(audioMuted: boolean, videoMuted: boolean) {
|
||||
setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
|
||||
}
|
||||
|
||||
function onLocalVolumeChanged(localVolume: number) {
|
||||
setState((prevState) => ({ ...prevState, localVolume }));
|
||||
}
|
||||
|
||||
function onUpdateCallFeed() {
|
||||
setState(getCallFeedState(callFeed));
|
||||
}
|
||||
|
||||
onUpdateCallFeed();
|
||||
|
||||
if (callFeed) {
|
||||
const onSpeaking = (speaking: boolean) => {
|
||||
otelGroupCallMembership?.onSpeaking(
|
||||
callFeed.getMember()!,
|
||||
callFeed.deviceId!,
|
||||
speaking
|
||||
);
|
||||
setState((prevState) => ({ ...prevState, speaking }));
|
||||
};
|
||||
|
||||
const onMuteStateChanged = (audioMuted: boolean, videoMuted: boolean) => {
|
||||
setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
|
||||
};
|
||||
|
||||
const onLocalVolumeChanged = (localVolume: number) => {
|
||||
setState((prevState) => ({ ...prevState, localVolume }));
|
||||
};
|
||||
|
||||
callFeed.on(CallFeedEvent.Speaking, onSpeaking);
|
||||
callFeed.on(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
|
||||
callFeed.on(CallFeedEvent.LocalVolumeChanged, onLocalVolumeChanged);
|
||||
callFeed.on(CallFeedEvent.NewStream, onUpdateCallFeed);
|
||||
callFeed.on(CallFeedEvent.Disposed, onUpdateCallFeed);
|
||||
}
|
||||
|
||||
onUpdateCallFeed();
|
||||
|
||||
return () => {
|
||||
if (callFeed) {
|
||||
callFeed.removeListener(CallFeedEvent.Speaking, onSpeaking);
|
||||
callFeed.removeListener(
|
||||
CallFeedEvent.MuteStateChanged,
|
||||
@@ -90,9 +98,9 @@ export function useCallFeed(callFeed: CallFeed | undefined): CallFeedState {
|
||||
onLocalVolumeChanged
|
||||
);
|
||||
callFeed.removeListener(CallFeedEvent.NewStream, onUpdateCallFeed);
|
||||
}
|
||||
};
|
||||
}, [callFeed]);
|
||||
}
|
||||
}, [callFeed, otelGroupCallMembership]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
221
test/otel/ObjectFlattener-test.ts
Normal file
221
test/otel/ObjectFlattener-test.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
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 }],
|
||||
]),
|
||||
},
|
||||
jitter: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", 2],
|
||||
["REMOTE_VIDEO_TRACK_ID", 50],
|
||||
]),
|
||||
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.jitter.REMOTE_AUDIO_TRACK_ID": 2,
|
||||
"matrix.stats.conn.jitter.REMOTE_VIDEO_TRACK_ID": 50,
|
||||
"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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
171
test/room/checkForParallelCalls-test.ts
Normal file
171
test/room/checkForParallelCalls-test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
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 { Mocked, mocked } from "jest-mock";
|
||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
|
||||
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
|
||||
|
||||
const withFakeTimers = (continuation: () => void) => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
continuation();
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
};
|
||||
|
||||
const withMockedPosthog = (
|
||||
continuation: (posthog: Mocked<PosthogAnalytics>) => void
|
||||
) => {
|
||||
const posthog = mocked({
|
||||
trackEvent: jest.fn(),
|
||||
} as unknown as PosthogAnalytics);
|
||||
const instanceSpy = jest
|
||||
.spyOn(PosthogAnalytics, "instance", "get")
|
||||
.mockReturnValue(posthog);
|
||||
try {
|
||||
continuation(posthog);
|
||||
} finally {
|
||||
instanceSpy.mockRestore();
|
||||
}
|
||||
};
|
||||
|
||||
const mockRoomState = (
|
||||
groupCallMemberContents: Record<string, unknown>[]
|
||||
): RoomState => {
|
||||
const stateEvents = groupCallMemberContents.map((content) => ({
|
||||
getContent: () => content,
|
||||
}));
|
||||
return { getStateEvents: () => stateEvents } as unknown as RoomState;
|
||||
};
|
||||
|
||||
test("checkForParallelCalls does nothing if all participants are in the same call", () => {
|
||||
withFakeTimers(() => {
|
||||
withMockedPosthog((posthog) => {
|
||||
const roomState = mockRoomState([
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": null, // invalid
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Android",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
null, // invalid
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Desktop",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
checkForParallelCalls(roomState);
|
||||
expect(posthog.trackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("checkForParallelCalls sends diagnostics to PostHog if there is a split-brain", () => {
|
||||
withFakeTimers(() => {
|
||||
withMockedPosthog((posthog) => {
|
||||
const roomState = mockRoomState([
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": "2",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Android",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": "1",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Desktop",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() + 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m.call_id": "2",
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: "Element Call",
|
||||
session_id: "a",
|
||||
expires_ts: Date.now() - 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
checkForParallelCalls(roomState);
|
||||
expect(posthog.trackEvent).toHaveBeenCalledWith({
|
||||
eventName: "ParallelCalls",
|
||||
participantsPerCall: {
|
||||
"1": 2,
|
||||
"2": 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
304
yarn.lock
304
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.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/matrix-sdk-crypto-js@^0.1.0-alpha.6":
|
||||
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"
|
||||
@@ -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"
|
||||
@@ -5584,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==
|
||||
@@ -6901,6 +7048,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 +8743,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 +9640,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 +10394,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==
|
||||
@@ -10235,9 +10418,14 @@ 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"
|
||||
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"
|
||||
@@ -10362,12 +10550,12 @@ 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":
|
||||
version "24.0.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/da03c3b529576a8fcde6f2c9a171fa6cca012830"
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#90234402a71955d60ca75a068e5450bdafed0b41":
|
||||
version "24.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/90234402a71955d60ca75a068e5450bdafed0b41"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.5"
|
||||
"@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.6"
|
||||
another-json "^0.2.0"
|
||||
bs58 "^5.0.0"
|
||||
content-type "^1.0.4"
|
||||
@@ -10619,6 +10807,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 +10894,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 +11281,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 +12143,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 +12878,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 +12931,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 +13284,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 +13578,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 +13904,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 +14102,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 +14556,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 +15079,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 +15155,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"
|
||||
|
||||
Reference in New Issue
Block a user