diff --git a/.oxlintrc.json b/.oxlintrc.json index 819e54396..d06f2a473 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -34,6 +34,7 @@ "/*\nCopyright %%CURRENT_YEAR%% Element Creations Ltd.\n\nSPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial\nPlease see LICENSE in the repository root for full details.\n*/\n\n" ], "element-call/no-observablescope-leak": "error", + "element-call/no-top-level-logger-get-child": "error", "jsdoc/empty-tags": "error", "jsdoc/check-property-names": "error", "jsdoc/require-param-description": "warn", diff --git a/eslint/NoTopLevelLoggerGetChild.js b/eslint/NoTopLevelLoggerGetChild.js new file mode 100644 index 000000000..3d9752c47 --- /dev/null +++ b/eslint/NoTopLevelLoggerGetChild.js @@ -0,0 +1,92 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { ESLintUtils } from "@typescript-eslint/utils"; + +/** + * Node types that introduce a new non-module scope. A getChild() call nested + * inside any of these is considered "not at the top level". + */ +const FUNCTION_OR_CLASS_TYPES = new Set([ + "FunctionDeclaration", + "FunctionExpression", + "ArrowFunctionExpression", + "ClassBody", +]); + +const rule = ESLintUtils.RuleCreator( + () => "https://github.com/element-hq/element-call", +)({ + name: "no-top-level-logger-get-child", + meta: { + type: "problem", + docs: { + description: + "Disallow calling logger.getChild() at the top level of a module." + + "`getChild` has to be called after the rageshake logger `init()`." + + "If it is called at the top level the child logger will never be setup for rageshakes.", + }, + messages: { + noTopLevelGetChild: + "Do not call logger.getChild() at the top level of a module; move it inside a function or class instead that gets called after rageshake logger `init()` is called.", + }, + schema: [], + }, + create(context) { + // Tracks the local binding names that refer to the logger imported from + // 'matrix-js-sdk/lib/logger', e.g. both `logger` and `rootLogger` in: + // import { logger } from "matrix-js-sdk/lib/logger"; + // import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; + const loggerNames = new Set(); + + return { + ImportDeclaration(node) { + if (node.source.value !== "matrix-js-sdk/lib/logger") return; + for (const specifier of node.specifiers) { + if ( + specifier.type === "ImportSpecifier" && + specifier.imported.name === "logger" + ) { + loggerNames.add(specifier.local.name); + } + } + }, + + CallExpression(node) { + // Must be a non-computed member expression call: something.getChild(...) + if ( + node.callee.type !== "MemberExpression" || + node.callee.computed || + node.callee.property.type !== "Identifier" || + node.callee.property.name !== "getChild" + ) + return; + + // The receiver must be one of the tracked logger names. + const object = node.callee.object; + if (object.type !== "Identifier" || !loggerNames.has(object.name)) + return; + + // Flag the call only when it is at module top level — i.e. there is no + // enclosing function or class body anywhere in the ancestor chain. + const ancestors = context.sourceCode.getAncestors(node); + const isTopLevel = !ancestors.some((a) => + FUNCTION_OR_CLASS_TYPES.has(a.type), + ); + + if (isTopLevel) { + context.report({ + messageId: "noTopLevelGetChild", + node, + }); + } + }, + }; + }, +}); + +export default rule; diff --git a/eslint/index.js b/eslint/index.js index ec050ad76..1f2e7629e 100644 --- a/eslint/index.js +++ b/eslint/index.js @@ -2,5 +2,7 @@ module.exports = { rules: { "copyright-header": require("./CopyrightHeader").default, "no-observablescope-leak": require("./NoObservableScopeLeak").default, + "no-top-level-logger-get-child": require("./NoTopLevelLoggerGetChild") + .default, }, }; diff --git a/sdk/helper.ts b/sdk/helper.ts index 47de4a939..9f7e02a7d 100644 --- a/sdk/helper.ts +++ b/sdk/helper.ts @@ -15,9 +15,8 @@ import { scan } from "rxjs"; import { type WidgetHelpers } from "../src/widget"; import { type LivekitRoomItem } from "../src/state/CallViewModel/CallViewModel"; -export const logger = rootLogger.getChild("[MatrixRTCSdk]"); - export const tryMakeSticky = (widget: WidgetHelpers): void => { + const logger = rootLogger.getChild("[MatrixRTCSdk]"); logger.info("try making sticky MatrixRTCSdk"); void widget.api .setAlwaysOnScreen(true) diff --git a/sdk/main.ts b/sdk/main.ts index b94a7eb51..a001af65c 100644 --- a/sdk/main.ts +++ b/sdk/main.ts @@ -52,7 +52,8 @@ import { getUrlParams } from "../src/UrlParams"; import { MuteStates } from "../src/state/MuteStates"; import { MediaDevices } from "../src/state/MediaDevices"; import { E2eeType } from "../src/e2ee/e2eeType"; -import { currentAndPrev, logger, TEXT_LK_TOPIC, tryMakeSticky } from "./helper"; +import { currentAndPrev, TEXT_LK_TOPIC, tryMakeSticky } from "./helper"; +import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { ElementWidgetActions, widget as _widget, @@ -104,6 +105,7 @@ export async function createMatrixRTCSdk( id: string = "", sticky: boolean = false, ): Promise { + const logger = rootLogger.getChild("[MatrixRTCSdk]"); const scope = new ObservableScope(); // widget client diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index 63a96755f..f1a66a28a 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -10,15 +10,15 @@ import { type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/lib/matrixrtc"; -import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; +import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; -const logger = rootLogger.getChild("[MatrixKeyProvider]"); export class MatrixKeyProvider extends BaseKeyProvider { private rtcSession?: MatrixRTCSession; - + private logger: Logger; public constructor() { super({ ratchetWindowSize: 10, keyringSize: 256 }); + this.logger = rootLogger.getChild("[MatrixKeyProvider]"); } public setRTCSession(rtcSession: MatrixRTCSession): void { @@ -60,12 +60,12 @@ export class MatrixKeyProvider extends BaseKeyProvider { encryptionKeyIndex, ); - logger.debug( + this.logger.debug( `Sent new key to livekit room=${this.rtcSession?.room.roomId} participantId=${rtcBackendIdentity} (before hash: ${membershipParts.userId}:${membershipParts.deviceId}) encryptionKeyIndex=${encryptionKeyIndex}`, ); }, (e) => { - logger.error( + this.logger.error( `Failed to create key material from buffer for livekit room=${this.rtcSession?.room.roomId} participantId before hash=${membershipParts.userId}:${membershipParts.deviceId} encryptionKeyIndex=${encryptionKeyIndex}`, e, ); diff --git a/src/livekit/MatrixAudioRenderer.tsx b/src/livekit/MatrixAudioRenderer.tsx index 10579c1b9..e3970e9f3 100644 --- a/src/livekit/MatrixAudioRenderer.tsx +++ b/src/livekit/MatrixAudioRenderer.tsx @@ -14,7 +14,7 @@ import { AudioTrack, type AudioTrackProps, } from "@livekit/components-react"; -import { logger } from "matrix-js-sdk/lib/logger"; +import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { useEarpieceAudioConfig } from "../MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; @@ -40,7 +40,6 @@ export interface MatrixAudioRendererProps { muted?: boolean; } -const prefixedLogger = logger.getChild("[MatrixAudioRenderer]"); /** * Takes care of handling remote participants’ audio tracks and makes sure that microphones and screen share are audible. * @@ -60,6 +59,7 @@ export function LivekitRoomAudioRenderer({ validIdentities, muted, }: MatrixAudioRendererProps): ReactNode { + const logger = rootLogger.getChild("[MatrixAudioRenderer]"); const tracks = useTracks( [ Track.Source.Microphone, @@ -80,7 +80,7 @@ export function LivekitRoomAudioRenderer({ if (!isValid) { // TODO make sure to also skip the warn logging for the local identity // Log that there is an invalid identity, that means that someone is publishing audio that is not expected to be in the call. - prefixedLogger.warn( + logger.warn( `Audio track ${ref.participant.identity} from ${url} has no matching matrix call member`, `current members: ${validIdentities.join()}`, `track will not get rendered`, diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 5872cfb44..bf5918724 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -94,8 +94,6 @@ declare module "react" { } } -const logger = rootLogger.getChild("[InCallView]"); - export interface ActiveCallProps extends Omit< InCallViewProps, "vm" | "livekitRoom" | "connState" | "footerVm" @@ -116,7 +114,7 @@ export const ActiveCall: FC = (props) => { const mediaDevices = useMediaDevices(); const trackProcessorState$ = useTrackProcessorObservable$(); useEffect(() => { - logger.info("START CALL VIEW SCOPE"); + rootLogger.info("START CALL VIEW SCOPE"); const scope = new ObservableScope(); const reactionsReader = new ReactionsReader(scope, props.rtcSession); const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } = @@ -218,6 +216,7 @@ export const InCallView: FC = ({ muteStates, onShareClick, }) => { + const logger = rootLogger.getChild("[InCallView]"); const { t } = useTranslation(); const { sendReaction, toggleRaisedHand } = useReactionsSender(); diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index bf534671d..b9ecbff50 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -467,7 +467,7 @@ declare global { // eslint-disable-next-line no-var, camelcase var mx_rage_initStoragePromise: Promise | undefined; } - +export let rageshakeLogger: Logger; /** * Configure rage shaking support for sending bug reports. * Modifies globals. @@ -477,7 +477,8 @@ export async function init(): Promise { global.mx_rage_logger = new ConsoleLogger(); // configure loglevel based loggers: - setLogExtension(logger, global.mx_rage_logger.log); + rageshakeLogger = logger; + setLogExtension(rageshakeLogger, global.mx_rage_logger.log); // intercept console logging so that we can get matrix_sdk logs: // this is nasty, but no logging hooks are provided diff --git a/src/state/CallViewModel/CallNotificationLifecycle.ts b/src/state/CallViewModel/CallNotificationLifecycle.ts index 4d7007b9e..59930fd64 100644 --- a/src/state/CallViewModel/CallNotificationLifecycle.ts +++ b/src/state/CallViewModel/CallNotificationLifecycle.ts @@ -39,8 +39,6 @@ import { type Behavior } from "../Behavior"; import { type Epoch, type ObservableScope } from "../ObservableScope"; import { type RoomMemberMap } from "./remoteMembers/MatrixMemberMetadata"; -const logger = rootLogger.getChild("[CallNotificationLifecycle]"); - export type AutoLeaveReason = "allOthersLeft" | "timeout" | "decline"; export interface RingAttempt { @@ -114,6 +112,7 @@ export function createCallNotificationLifecycle$({ */ autoLeave$: Observable; } { + const logger = rootLogger.getChild("[CallNotificationLifecycle]"); let ringAttempts$: Observable = NEVER; if (options.waitForCallPickup) ringAttempts$ = sentCallNotification$.pipe( diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 27676cb50..cd16f6839 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -40,7 +40,7 @@ import { timer, takeUntil, } from "rxjs"; -import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; +import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { MembershipManagerEvent, type LivekitTransportConfig, @@ -157,7 +157,6 @@ import { } from "../media/RingingMediaViewModel.ts"; import { type GridTileViewModel } from "../TileViewModel.ts"; -const logger = rootLogger.getChild("[CallViewModel]"); //TODO // Larger rename // member,membership -> rtcMember @@ -411,6 +410,7 @@ export function createCallViewModel$( reactionsSubject$: Observable>, trackProcessorState$: Behavior, ): CallViewModel { + const logger = rootLogger.getChild("[CallViewModel]"); const client = matrixRoom.client; const userId = client.getUserId(); const deviceId = client.getDeviceId(); @@ -420,6 +420,7 @@ export function createCallViewModel$( const livekitKeyProvider = getE2eeKeyProvider( options.encryptionSystem, matrixRTCSession, + logger, ); // matrix_rtc_mode in config.json overrides the user's Developer Settings choice. // It is validated at config load (src/config/Config.ts) so the cast is safe. @@ -1797,6 +1798,7 @@ export function createCallViewModel$( function getE2eeKeyProvider( e2eeSystem: EncryptionSystem, rtcSession: MatrixRTCSession, + logger: Logger, ): BaseKeyProvider | undefined { if (e2eeSystem.kind === E2eeType.NONE) return undefined; diff --git a/src/state/CallViewModel/localMember/HomeserverConnected.ts b/src/state/CallViewModel/localMember/HomeserverConnected.ts index 227c21c31..45e9f6cbb 100644 --- a/src/state/CallViewModel/localMember/HomeserverConnected.ts +++ b/src/state/CallViewModel/localMember/HomeserverConnected.ts @@ -31,11 +31,6 @@ import { type ObservableScope } from "../../ObservableScope"; import { type Behavior } from "../../Behavior"; import { type NodeStyleEventEmitter } from "../../../utils/test"; -/** - * Logger instance (scoped child) for homeserver connection updates. - */ -const logger = rootLogger.getChild("[HomeserverConnected]"); - export type HomeserverDisconnectReason = "sync" | "membership" | "probablyLeft"; export interface HomeserverConnected { @@ -70,6 +65,7 @@ export function createHomeserverConnected$( Pick, gracePeriodMs?: number, ): HomeserverConnected { + const logger = rootLogger.getChild("[HomeserverConnected]"); // Get grace period from parameter or config (default 10000ms) const graceMs = gracePeriodMs ?? Config.get().sync_disconnect_grace_period_ms; diff --git a/src/state/CallViewModel/localMember/LocalTransport.ts b/src/state/CallViewModel/localMember/LocalTransport.ts index 10ea79c44..e255ca946 100644 --- a/src/state/CallViewModel/localMember/LocalTransport.ts +++ b/src/state/CallViewModel/localMember/LocalTransport.ts @@ -25,7 +25,7 @@ import { switchMap, tap, } from "rxjs"; -import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; +import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger"; import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; @@ -46,8 +46,6 @@ import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers import { customLivekitUrl } from "../../../settings/settings.ts"; import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts"; -const logger = rootLogger.getChild("[LocalTransport]"); - /* * It figures out “which LiveKit focus URL/alias the local user should use,” * optionally aligning with the oldest member, and ensures the SFU path is primed @@ -140,9 +138,14 @@ export const createLocalTransport$ = ({ forceJwtEndpoint, delayId$, }: Props): LocalTransport => { + const logger = rootLogger.getChild("[LocalTransport]"); // The LiveKit transport in use by the oldest RTC membership. `null` when the // oldest member has no such transport. - const oldestMemberTransport$ = observerOldestMembership$(scope, memberships$); + const oldestMemberTransport$ = observerOldestMembership$( + scope, + memberships$, + logger, + ); const transportDiscovery = new RtcTransportAutoDiscovery({ client: client, @@ -190,6 +193,7 @@ export const createLocalTransport$ = ({ roomId, client, delayId ?? undefined, + logger, ); } catch (e) { logger.error( @@ -209,6 +213,7 @@ export const createLocalTransport$ = ({ client, ownMembershipIdentity, roomId, + logger, ); } @@ -248,6 +253,7 @@ export const createLocalTransport$ = ({ function observerOldestMembership$( scope: ObservableScope, memberships$: Behavior>, + logger: Logger, ): Behavior { return scope.behavior( memberships$.pipe( @@ -307,6 +313,7 @@ async function doOpenIdAndJWTFromUrl( > & OpenIDClientParts, delayId?: string, + logger?: Logger, ): Promise { const sfuConfig = await getSFUConfigWithOpenID( client, @@ -337,6 +344,7 @@ function observeLocalTransportForOldestMembership( OpenIDClientParts, ownMembershipIdentity: CallMembershipIdentityParts, roomId: string, + logger: Logger, ): LocalTransport { // Ensure we can authenticate with the SFU. const authenticatedOldestMemberTransport$ = oldestMemberTransport$.pipe( @@ -355,6 +363,7 @@ function observeLocalTransportForOldestMembership( roomId, client, undefined, + logger, ), ).pipe( catchError((e: unknown) => { diff --git a/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.ts b/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.ts index 19c6fdcd5..6a448128c 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.ts @@ -22,8 +22,6 @@ import { } from "../../../utils/displayname"; import { type Behavior } from "../../Behavior"; -const logger = rootLogger.getChild("[MatrixMemberMetadata]"); - export type RoomMemberMap = Map< string, Pick @@ -67,6 +65,7 @@ export const memberDisplaynames$ = ( memberships$: Behavior[]>, roomMembers$: Behavior, ): Behavior> => { + const logger = rootLogger.getChild("[MatrixMemberMetadata]"); // This map tracks userIds that at some point needed disambiguation. // This is a memory leak bound to the number of participants. // A call application will always increase the memory if there have been more members in a call.