Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -41,8 +41,11 @@ import {
import { RoomType } from '$types/matrix/room';
import { CreateRoomTypeSelector } from '$components/create-room/CreateRoomTypeSelector';
import { getRoomIconSrc } from '$utils/room';
import { createDebugLogger } from '$utils/debugLogger';
import { ErrorCode } from '../../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 @@ export function CreateRoomForm({
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 @@ export function CreateRoomForm({
allowFederation: federation,
additionalCreators: allowAdditionalCreators ? additionalCreators : undefined,
}).then((roomId) => {
debugLog.info('ui', 'Room created successfully', {
roomId,
roomName,
access,
type,
});
if (alive()) {
onCreate?.(roomId);
}
Expand Down
30 changes: 29 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 @@ -15,20 +15,43 @@ import { useRoomMembers } from '$hooks/useRoomMembers';
import { CallView } from '$features/call/CallView';
import { WidgetsDrawer } from '$features/widgets/WidgetsDrawer';
import { callChatAtom } from '$state/callEmbed';
import { createDebugLogger } from '$utils/debugLogger';
import { RoomViewHeader } from './RoomViewHeader';
import { MembersDrawer } from './MembersDrawer';
import { RoomView } from './RoomView';
import { CallChatView } from './CallChatView';

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,
});
}, [isWidgetDrawerOpen, room.roomId]);
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
const chat = useAtomValue(callChatAtom);
Expand All @@ -47,6 +70,11 @@ export function Room() {

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
55 changes: 46 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 { useImagePackRooms } from '$hooks/useImagePackRooms';
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 getReplyContent = (replyDraft: IReplyDraft | undefined): IEventRelation =>
};

const log = createLogger('RoomInput');
const debugLog = createDebugLogger('RoomInput');
interface ReplyEventContent {
'm.relates_to'?: IEventRelation;
}
Expand Down Expand Up @@ -422,10 +424,24 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(

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)
.then((res) => {
debugLog.info('message', 'Uploaded file message sent', {
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),
});
log.error('failed to send uploaded message', { roomId }, error);
throw error;
})
)
);
};
Expand Down Expand Up @@ -569,18 +585,39 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
} 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,
});
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),
});
// 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,
});
})
.catch((error: unknown) => {
debugLog.error('message', 'Failed to send message', {
roomId,
error: error instanceof Error ? error.message : String(error),
});
log.error('failed to send message', { roomId }, error);
});
}
}, [
editor,
Expand Down
69 changes: 67 additions & 2 deletions 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,29 @@ 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 });
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [room.roomId, eventId]); // Only log on mount/unmount - intentionally capturing initial values

// 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 +778,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 @@ -753,10 +799,11 @@ export function RoomTimeline({
},
});
},
[alive, setAtBottom]
[alive, setAtBottom, room.roomId]
),
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 +877,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 +901,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 +942,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 +1032,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