Skip to content
5 changes: 5 additions & 0 deletions .changeset/feat-internal-debug-logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
sable: minor
---

Add internal debug logging system with viewer UI, realtime updates, and instrumentation across sync, timeline, and messaging
7 changes: 6 additions & 1 deletion src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { MatrixError } from '$types/matrix-sdk';
import { useMatrixClient } from '$hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback';
import { stopPropagation } from '$utils/keyboard';
import { createDebugLogger } from '$utils/debugLogger';

const debugLog = createDebugLogger('LeaveRoomPrompt');

type LeaveRoomPromptProps = {
roomId: string;
Expand All @@ -31,6 +34,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro

const [leaveState, leaveRoom] = useAsyncCallback<undefined, MatrixError, []>(
useCallback(async () => {
debugLog.info('ui', 'Leave room button clicked', { roomId });
mx.leave(roomId);
}, [mx, roomId])
);
Expand All @@ -41,9 +45,10 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro

useEffect(() => {
if (leaveState.status === AsyncStatus.Success) {
debugLog.info('ui', 'Successfully left room', { roomId });
onDone();
}
}, [leaveState, onDone]);
}, [leaveState, onDone, roomId]);

return (
<Overlay open backdrop={<OverlayBackdrop />}>
Expand Down
19 changes: 19 additions & 0 deletions src/app/features/create-room/CreateRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
import { CreateRoomTypeSelector } from '$components/create-room/CreateRoomTypeSelector';
import { getRoomIconSrc } from '$utils/room';
import { ErrorCode } from '../../cs-errorcode';
import { createDebugLogger } from '$utils/debugLogger';

Check failure on line 45 in src/app/features/create-room/CreateRoom.tsx

View workflow job for this annotation

GitHub Actions / Lint

`$utils/debugLogger` import should occur before import of `../../cs-errorcode`

const debugLog = createDebugLogger('CreateRoom');

const getCreateRoomAccessToIcon = (access: CreateRoomAccess, type?: CreateRoomType) => {
const isVoiceRoom = type === CreateRoomType.VoiceRoom;
Expand Down Expand Up @@ -139,6 +142,16 @@
let roomType: RoomType | undefined;
if (type === CreateRoomType.VoiceRoom) roomType = RoomType.Call;

debugLog.info('ui', 'Create room button clicked', {
roomName,
access,
type,
publicRoom,
encryption,
hasParent: !!space,
parentRoomId: space?.roomId,
});

create({
version: selectedRoomVersion,
type: roomType,
Expand All @@ -152,6 +165,12 @@
allowFederation: federation,
additionalCreators: allowAdditionalCreators ? additionalCreators : undefined,
}).then((roomId) => {
debugLog.info('ui', 'Room created successfully', {
roomId,
roomName,
access,
type,
});
if (alive()) {
onCreate?.(roomId);
}
Expand Down
27 changes: 26 additions & 1 deletion src/app/features/room/Room.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { Box, Line } from 'folds';
import { useParams } from 'react-router-dom';
import { isKeyHotkey } from 'is-hotkey';
Expand All @@ -19,16 +19,36 @@
import { MembersDrawer } from './MembersDrawer';
import { RoomView } from './RoomView';
import { CallChatView } from './CallChatView';
import { createDebugLogger } from '$utils/debugLogger';

Check failure on line 22 in src/app/features/room/Room.tsx

View workflow job for this annotation

GitHub Actions / Lint

`$utils/debugLogger` import should occur before import of `./RoomViewHeader`

const debugLog = createDebugLogger('Room');

export function Room() {
const { eventId } = useParams();
const room = useRoom();
const mx = useMatrixClient();

// Log room mount
useEffect(() => {
debugLog.info('ui', 'Room component mounted', { roomId: room.roomId, eventId });
return () => {
debugLog.info('ui', 'Room component unmounted', { roomId: room.roomId });
};
}, [room.roomId, eventId]);

const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const [isWidgetDrawerOpen] = useSetting(settingsAtom, 'isWidgetDrawer');
const [hideReads] = useSetting(settingsAtom, 'hideReads');
const screenSize = useScreenSizeContext();

// Log drawer state changes
useEffect(() => {
debugLog.debug('ui', 'Members drawer state changed', { roomId: room.roomId, isOpen: isDrawer });
}, [isDrawer, room.roomId]);

useEffect(() => {
debugLog.debug('ui', 'Widgets drawer state changed', { roomId: room.roomId, isOpen: isWidgetDrawerOpen });

Check failure on line 50 in src/app/features/room/Room.tsx

View workflow job for this annotation

GitHub Actions / Lint

Replace `·roomId:·room.roomId,·isOpen:·isWidgetDrawerOpen` with `⏎······roomId:·room.roomId,⏎······isOpen:·isWidgetDrawerOpen,⏎···`
}, [isWidgetDrawerOpen, room.roomId]);
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
const chat = useAtomValue(callChatAtom);
Expand All @@ -47,6 +67,11 @@

const callView = room.isCallRoom();

// Log call view state
useEffect(() => {
debugLog.debug('ui', 'Room view mode', { roomId: room.roomId, callView, chatOpen: chat });
}, [callView, chat, room.roomId]);

return (
<PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes">
Expand Down
35 changes: 26 additions & 9 deletions src/app/features/room/RoomInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
import { useComposingCheck } from '$hooks/useComposingCheck';
import { useSableCosmetics } from '$hooks/useSableCosmetics';
import { createLogger } from '$utils/debug';
import { createDebugLogger } from '$utils/debugLogger';
import FocusTrap from 'focus-trap-react';
import { useQueryClient } from '@tanstack/react-query';
import {
Expand Down Expand Up @@ -176,6 +177,7 @@
};

const log = createLogger('RoomInput');
const debugLog = createDebugLogger('RoomInput');
interface ReplyEventContent {
'm.relates_to'?: IEventRelation;
}
Expand Down Expand Up @@ -422,10 +424,16 @@

await Promise.all(
contents.map((content) =>
mx.sendMessage(roomId, content as any).catch((error: unknown) => {
log.error('failed to send uploaded message', { roomId }, error);
throw error;
})
mx.sendMessage(roomId, content as any)

Check failure on line 427 in src/app/features/room/RoomInput.tsx

View workflow job for this annotation

GitHub Actions / Lint

Insert `⏎············`
.then((res) => {
debugLog.info('message', 'Uploaded file message sent', { roomId, eventId: res.event_id, msgtype: content.msgtype });

Check failure on line 429 in src/app/features/room/RoomInput.tsx

View workflow job for this annotation

GitHub Actions / Lint

Replace `·roomId,·eventId:·res.event_id,·msgtype:·content.msgtype` with `⏎················roomId,⏎················eventId:·res.event_id,⏎················msgtype:·content.msgtype,⏎·············`
return res;
})
.catch((error: unknown) => {
debugLog.error('message', 'Failed to send uploaded file message', { roomId, error: error instanceof Error ? error.message : String(error) });

Check failure on line 433 in src/app/features/room/RoomInput.tsx

View workflow job for this annotation

GitHub Actions / Lint

Replace `·roomId,·error:·error·instanceof·Error·?·error.message·:·String(error)` with `⏎················roomId,⏎················error:·error·instanceof·Error·?·error.message·:·String(error),⏎·············`
log.error('failed to send uploaded message', { roomId }, error);
throw error;
})
)
);
};
Expand Down Expand Up @@ -569,18 +577,27 @@
} else if (editingScheduledDelayId) {
try {
await cancelDelayedEvent(mx, editingScheduledDelayId);
mx.sendMessage(roomId, content as any);
debugLog.info('message', 'Sending message after cancelling scheduled event', { roomId, scheduledDelayId: editingScheduledDelayId });

Check failure on line 580 in src/app/features/room/RoomInput.tsx

View workflow job for this annotation

GitHub Actions / Lint

Replace `·roomId,·scheduledDelayId:·editingScheduledDelayId` with `⏎············roomId,⏎············scheduledDelayId:·editingScheduledDelayId,⏎·········`
const res = await mx.sendMessage(roomId, content as any);
debugLog.info('message', 'Message sent successfully', { roomId, eventId: res.event_id });
invalidate();
setEditingScheduledDelayId(null);
resetInput();
} catch {
} catch (error) {
debugLog.error('message', 'Failed to send message after cancelling scheduled event', { roomId, error: error instanceof Error ? error.message : String(error) });

Check failure on line 587 in src/app/features/room/RoomInput.tsx

View workflow job for this annotation

GitHub Actions / Lint

Replace `·roomId,·error:·error·instanceof·Error·?·error.message·:·String(error)` with `⏎············roomId,⏎············error:·error·instanceof·Error·?·error.message·:·String(error),⏎·········`
// Cancel failed — leave state intact for retry
}
} else {
resetInput();
mx.sendMessage(roomId, content as any).catch((error: unknown) => {
log.error('failed to send message', { roomId }, error);
});
debugLog.info('message', 'Sending message', { roomId, msgtype: (content as any).msgtype });
mx.sendMessage(roomId, content as any)
.then((res) => {
debugLog.info('message', 'Message sent successfully', { roomId, eventId: res.event_id });

Check failure on line 595 in src/app/features/room/RoomInput.tsx

View workflow job for this annotation

GitHub Actions / Lint

Replace `·roomId,·eventId:·res.event_id` with `⏎··············roomId,⏎··············eventId:·res.event_id,⏎···········`
})
.catch((error: unknown) => {
debugLog.error('message', 'Failed to send message', { roomId, error: error instanceof Error ? error.message : String(error) });

Check failure on line 598 in src/app/features/room/RoomInput.tsx

View workflow job for this annotation

GitHub Actions / Lint

Replace `·roomId,·error:·error·instanceof·Error·?·error.message·:·String(error)` with `⏎··············roomId,⏎··············error:·error·instanceof·Error·?·error.message·:·String(error),⏎···········`
log.error('failed to send message', { roomId }, error);
});
}
}, [
editor,
Expand Down
66 changes: 65 additions & 1 deletion src/app/features/room/RoomTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,12 @@ import { useRoomPermissions } from '$hooks/useRoomPermissions';
import { useGetMemberPowerTag } from '$hooks/useMemberPowerTag';
import { profilesCacheAtom } from '$state/userRoomProfile';
import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze';
import { createDebugLogger } from '$utils/debugLogger';
import * as css from './RoomTimeline.css';
import { EncryptedContent, Event, ForwardedMessageProps, Message, Reactions } from './message';

const debugLog = createDebugLogger('RoomTimeline');

const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => (
<Box
Expand Down Expand Up @@ -399,6 +402,11 @@ const useTimelinePagination = (
fetching = true;
if (alive()) {
(backwards ? setBackwardStatus : setForwardStatus)('loading');
debugLog.info('timeline', 'Timeline pagination started', {
direction: backwards ? 'backward' : 'forward',
eventsLoaded: getTimelinesEventsCount(lTimelines),
hasToken: !!paginationToken,
});
}
try {
const [err] = await to(
Expand All @@ -410,6 +418,10 @@ const useTimelinePagination = (
if (err) {
if (alive()) {
(backwards ? setBackwardStatus : setForwardStatus)('error');
debugLog.error('timeline', 'Timeline pagination failed', {
direction: backwards ? 'backward' : 'forward',
error: err instanceof Error ? err.message : String(err),
});
}
return;
}
Expand All @@ -428,6 +440,10 @@ const useTimelinePagination = (
if (alive()) {
recalibratePagination(lTimelines, timelinesEventsCount, backwards);
(backwards ? setBackwardStatus : setForwardStatus)('idle');
debugLog.info('timeline', 'Timeline pagination completed', {
direction: backwards ? 'backward' : 'forward',
totalEventsNow: getTimelinesEventsCount(lTimelines),
});
}
} finally {
fetching = false;
Expand Down Expand Up @@ -689,6 +705,28 @@ export function RoomTimeline({
);
const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines);
const liveTimelineLinked = timeline.linkedTimelines.at(-1) === getLiveTimeline(room);

// Log timeline component mount/unmount
useEffect(() => {
debugLog.info('timeline', 'Timeline mounted', {
roomId: room.roomId,
eventId,
initialEventsCount: eventsLength,
liveTimelineLinked,
});
return () => {
debugLog.info('timeline', 'Timeline unmounted', { roomId: room.roomId });
};
}, [room.roomId, eventId]); // Only log on mount/unmount

// Log live timeline linking state changes
useEffect(() => {
debugLog.debug('timeline', 'Live timeline link state changed', {
roomId: room.roomId,
liveTimelineLinked,
eventsLength,
});
}, [liveTimelineLinked, room.roomId, eventsLength]);
const canPaginateBack =
typeof timeline.linkedTimelines[0]?.getPaginationToken(Direction.Backward) === 'string';
const rangeAtStart = timeline.range.start === 0;
Expand Down Expand Up @@ -739,6 +777,13 @@ export function RoomTimeline({
if (!alive()) return;
const evLength = getTimelinesEventsCount(lTimelines);

debugLog.info('timeline', 'Loading event timeline', {
roomId: room.roomId,
eventId: evtId,
totalEvents: evLength,
focusIndex: evtAbsIndex,
});

setAtBottom(false);
setFocusItem({
index: evtAbsIndex,
Expand All @@ -757,6 +802,7 @@ export function RoomTimeline({
),
useCallback(() => {
if (!alive()) return;
debugLog.info('timeline', 'Resetting timeline to initial state', { roomId: room.roomId });
setTimeline(getInitialTimeline(room));
scrollToBottomRef.current.count += 1;
scrollToBottomRef.current.smooth = false;
Expand Down Expand Up @@ -830,6 +876,12 @@ export function RoomTimeline({
highlight = true,
onScroll: ((scrolled: boolean) => void) | undefined = undefined
) => {
debugLog.info('timeline', 'Jumping to event', {
roomId: room.roomId,
eventId: evtId,
highlight,
});

const evtTimeline = getEventTimeline(room, evtId);
const absoluteIndex =
evtTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, evtTimeline, evtId);
Expand All @@ -848,7 +900,16 @@ export function RoomTimeline({
scrollTo: !scrolled,
highlight,
});
debugLog.debug('timeline', 'Event found in current timeline', {
roomId: room.roomId,
eventId: evtId,
index: absoluteIndex,
});
} else {
debugLog.debug('timeline', 'Event not in current timeline, loading timeline', {
roomId: room.roomId,
eventId: evtId,
});
loadEventTimeline(evtId);
}
},
Expand Down Expand Up @@ -880,6 +941,7 @@ export function RoomTimeline({
// "Jump to Latest" button to stick permanently. Forcing atBottom here is
// correct: TimelineRefresh always reinits to the live end, so the user
// should be repositioned to the bottom regardless.
debugLog.info('timeline', 'Live timeline refresh triggered', { roomId: room.roomId });
setTimeline(getInitialTimeline(room));
setAtBottom(true);
scrollToBottomRef.current.count += 1;
Expand Down Expand Up @@ -969,16 +1031,18 @@ export function RoomTimeline({

if (targetEntry.isIntersecting) {
// User has reached the bottom
debugLog.debug('timeline', 'Scrolled to bottom', { roomId: room.roomId });
setAtBottom(true);
if (atLiveEndRef.current && document.hasFocus()) {
tryAutoMarkAsRead();
}
} else {
// User has intentionally scrolled up.
debugLog.debug('timeline', 'Scrolled away from bottom', { roomId: room.roomId });
setAtBottom(false);
}
},
[tryAutoMarkAsRead, setAtBottom]
[tryAutoMarkAsRead, setAtBottom, room.roomId]
),
useCallback(
() => ({
Expand Down
Loading
Loading