From 34168d231c43e238302f016ef0cc0d371da859ef Mon Sep 17 00:00:00 2001 From: BP602 <3460479+BP602@users.noreply.github.com> Date: Sun, 21 Sep 2025 14:27:41 +0200 Subject: [PATCH 1/5] chore: add logging to 7tv cosmetics flow --- src/renderer/src/providers/ChatProvider.jsx | 24 +++++++++ .../src/providers/CosmeticsProvider.jsx | 17 +++++++ utils/services/seventv/sharedStvWebSocket.js | 51 ++++++++++++++++++- 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/providers/ChatProvider.jsx b/src/renderer/src/providers/ChatProvider.jsx index 5b48457..52ec3dc 100644 --- a/src/renderer/src/providers/ChatProvider.jsx +++ b/src/renderer/src/providers/ChatProvider.jsx @@ -1893,6 +1893,15 @@ const useChatStore = create((set, get) => ({ handleStvMessage: (chatroomId, eventDetail) => { const { type, body } = eventDetail; + console.log( + `[ChatProvider] Received 7TV event ${type} for ${chatroomId ?? 'broadcast'}`, + { + badgeCount: body?.badges?.length, + paintCount: body?.paints?.length, + entitlementUser: body?.object?.user?.username, + }, + ); + switch (type) { case "connection_established": break; @@ -1900,11 +1909,26 @@ const useChatStore = create((set, get) => ({ get().handleEmoteSetUpdate(chatroomId, body); break; case "cosmetic.create": + console.log( + `[ChatProvider] Applying cosmetic catalog update for ${chatroomId ?? 'all chatrooms'}`, + { + badges: body?.badges?.length, + paints: body?.paints?.length, + }, + ); useCosmeticsStore?.getState()?.addCosmetics(body); break; case "entitlement.create": { const username = body?.object?.user?.connections?.find((c) => c.platform === "KICK")?.username; const transformedUsername = username?.replaceAll("-", "_").toLowerCase(); + console.log( + `[ChatProvider] Processing entitlement for ${transformedUsername || 'unknown user'}`, + { + badgeId: body?.object?.user?.style?.badge_id, + paintId: body?.object?.user?.style?.paint_id, + chatroomId, + }, + ); useCosmeticsStore?.getState()?.addUserStyle(transformedUsername, body); break; } diff --git a/src/renderer/src/providers/CosmeticsProvider.jsx b/src/renderer/src/providers/CosmeticsProvider.jsx index 9f064fa..15d42e7 100644 --- a/src/renderer/src/providers/CosmeticsProvider.jsx +++ b/src/renderer/src/providers/CosmeticsProvider.jsx @@ -12,6 +12,15 @@ const useCosmeticsStore = create((set, get) => ({ const transformedUsername = username.toLowerCase(); const userStyle = body.object.user; + console.log( + `[CosmeticsStore] Upserting style for ${transformedUsername}`, + { + badgeId: userStyle?.style?.badge_id, + paintId: userStyle?.style?.paint_id, + entitlementId: body?.object?.id, + }, + ); + set((state) => { const currentStyle = state.userStyles[transformedUsername] || {}; if (currentStyle.badgeId === body.object.user.style.badge_id && currentStyle.paintId === body.object.user.style.paint_id) @@ -54,6 +63,14 @@ const useCosmeticsStore = create((set, get) => ({ }, addCosmetics: (body) => { + console.log( + `[CosmeticsStore] Updating global cosmetics catalog`, + { + badges: body?.badges?.length, + paints: body?.paints?.length, + }, + ); + set(() => { const newState = { globalCosmetics: { diff --git a/utils/services/seventv/sharedStvWebSocket.js b/utils/services/seventv/sharedStvWebSocket.js index 387ba23..9d9d30f 100644 --- a/utils/services/seventv/sharedStvWebSocket.js +++ b/utils/services/seventv/sharedStvWebSocket.js @@ -174,6 +174,10 @@ class SharedStvWebSocket extends EventTarget { stvEmoteSetId, }); + console.log( + `[Shared7TV]: Registered chatroom ${chatroomId} (kick=${channelKickID}, stvUser=${stvId}, stvSet=${stvEmoteSetId})`, + ); + // If we're already connected, subscribe to this chatroom's events if (this.connectionState === 'connected') { this.subscribeToChatroomEvents(chatroomId); @@ -398,6 +402,10 @@ class SharedStvWebSocket extends EventTarget { return; } + console.log( + `[Shared7TV]: Preparing subscriptions for chatroom ${chatroomId} (kick=${chatroomData.channelKickID}, emoteSet=${chatroomData.stvEmoteSetId})`, + ); + const { channelKickID, stvEmoteSetId } = chatroomData; // Validate IDs for specific subscriptions @@ -474,6 +482,9 @@ class SharedStvWebSocket extends EventTarget { const eventKey = `cosmetic.*:${channelKickID}`; if (this.subscribedEvents.has(eventKey)) { + console.log( + `[Shared7TV]: Cosmetic subscription already active for Kick channel ${channelKickID} (chatroom ${chatroomId})`, + ); return; } @@ -502,6 +513,9 @@ class SharedStvWebSocket extends EventTarget { const eventKey = `entitlement.*:${channelKickID}`; if (this.subscribedEvents.has(eventKey)) { + console.log( + `[Shared7TV]: Entitlement subscription already active for Kick channel ${channelKickID} (chatroom ${chatroomId})`, + ); return; } @@ -575,6 +589,14 @@ class SharedStvWebSocket extends EventTarget { // Find which chatroom this event belongs to const chatroomId = this.findChatroomForEvent(body, type); + console.log( + `[Shared7TV]: Received ${type} for ${chatroomId === null ? 'broadcast' : `chatroom ${chatroomId}`}`, + { + channelContext: body?.context || body?.condition || null, + entitlementUser: body?.object?.user?.username, + }, + ); + switch (type) { case "user.update": this.dispatchEvent( @@ -603,6 +625,14 @@ class SharedStvWebSocket extends EventTarget { case "cosmetic.create": updateCosmetics(body); + console.log( + `[Shared7TV]: Forwarding cosmetic catalog update to ${chatroomId === null ? 'all chatrooms' : chatroomId}`, + { + badgeCount: cosmetics?.badges?.length, + paintCount: cosmetics?.paints?.length, + }, + ); + this.dispatchEvent( new CustomEvent("message", { detail: { @@ -616,6 +646,14 @@ class SharedStvWebSocket extends EventTarget { case "entitlement.create": if (body.kind === 10) { + console.log( + `[Shared7TV]: Forwarding entitlement for ${body?.object?.user?.username || 'unknown user'}`, + { + chatroomId, + badgeId: body?.object?.user?.style?.badge_id, + paintId: body?.object?.user?.style?.paint_id, + }, + ); this.dispatchEvent( new CustomEvent("message", { detail: { @@ -637,7 +675,7 @@ class SharedStvWebSocket extends EventTarget { findChatroomForEvent(body, type) { // Try to identify which chatroom this event belongs to // This is a best-effort approach since 7TV events don't always include channel context - + // For user events, broadcast to all chatrooms if (type.startsWith("user.")) { return null; // null means broadcast to all chatrooms @@ -654,6 +692,17 @@ class SharedStvWebSocket extends EventTarget { // For cosmetic and entitlement events, they should include channel context // but if not, we'll broadcast to all chatrooms + if (type.startsWith("cosmetic.") || type.startsWith("entitlement.")) { + const contextId = body?.context?.id || body?.condition?.id || null; + console.log( + `[Shared7TV]: Unable to directly map ${type} to a chatroom, broadcasting`, + { + contextId, + knownChatrooms: Array.from(this.chatrooms.values()).map((data) => data.channelKickID), + }, + ); + } + return null; } From b6dbb8aa3ce7b9567c5c071d5de21037e82f1ad7 Mon Sep 17 00:00:00 2001 From: BP602 Date: Sun, 21 Sep 2025 19:56:49 +0200 Subject: [PATCH 2/5] feat(diagnostics): add comprehensive 7TV cosmetics tracing and test functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added detailed diagnostic logging throughout 7TV cosmetics flow and test functions to identify where the fetch → receive → store → apply chain breaks. --- src/renderer/src/providers/ChatProvider.jsx | 50 ++++- .../src/providers/CosmeticsProvider.jsx | 165 +++++++++++++-- utils/services/seventv/sharedStvWebSocket.js | 198 +++++++++++++++++- utils/services/seventv/stvWebsocket.js | 134 +++++++++++- 4 files changed, 521 insertions(+), 26 deletions(-) diff --git a/src/renderer/src/providers/ChatProvider.jsx b/src/renderer/src/providers/ChatProvider.jsx index 52ec3dc..fae0927 100644 --- a/src/renderer/src/providers/ChatProvider.jsx +++ b/src/renderer/src/providers/ChatProvider.jsx @@ -551,7 +551,9 @@ const useChatStore = create((set, get) => ({ if (stvPresenceUpdates.has(userId)) { const lastUpdateTime = stvPresenceUpdates.get(userId); - console.log("[7tv Presence]: Last update time for chatroom:", userId, lastUpdateTime, stvPresenceUpdates); + if (window.__KT_TELEMETRY_UTILS__?.shouldLogDebug?.()) { + console.log("[7tv Presence]: Last update time for chatroom:", userId, lastUpdateTime, stvPresenceUpdates); + } if (currentTime - lastUpdateTime < PRESENCE_UPDATE_INTERVAL) { return; } @@ -1509,10 +1511,23 @@ const useChatStore = create((set, get) => ({ // 7TV event handlers onStvMessage: (event) => { try { - const { chatroomId } = event.detail; + const { chatroomId, type, body } = event.detail; + console.log(`[ChatProvider] Received 7TV event from shared WebSocket`, { + type, + chatroomId, + hasBody: !!body, + entitlementUser: body?.object?.user?.username, + badgeId: body?.object?.user?.style?.badge_id, + paintId: body?.object?.user?.style?.paint_id, + badgeCount: body?.badges?.length, + paintCount: body?.paints?.length + }); + if (chatroomId) { + console.log(`[ChatProvider] Routing event ${type} to specific chatroom: ${chatroomId}`); get().handleStvMessage(chatroomId, event.detail); } else { + console.log(`[ChatProvider] Broadcasting event ${type} to all chatrooms (${chatrooms.length} total)`); // Broadcast to all chatrooms if no specific chatroom chatrooms.forEach(chatroom => { get().handleStvMessage(chatroom.id, event.detail); @@ -1625,7 +1640,9 @@ const useChatStore = create((set, get) => ({ get().connectToChatroom(chatroom); // Connect to 7TV WebSocket - get().connectToStvWebSocket(chatroom); + // DISABLED: Using shared connection system via connectionManager.initializeConnections + console.log(`[ChatProvider] Skipping individual 7TV connection for chatroom ${chatroom.id} - using shared connection system`); + // get().connectToStvWebSocket(chatroom); } }); }, @@ -1916,7 +1933,16 @@ const useChatStore = create((set, get) => ({ paints: body?.paints?.length, }, ); - useCosmeticsStore?.getState()?.addCosmetics(body); + const cosmetics = useCosmeticsStore?.getState()?.addCosmetics; + if (cosmetics) { + console.log(`[ChatProvider] Calling CosmeticsStore.addCosmetics with body:`, { + badges: body?.badges?.length, + paints: body?.paints?.length + }); + cosmetics(body); + } else { + console.error(`[ChatProvider] CosmeticsStore.addCosmetics method not available!`); + } break; case "entitlement.create": { const username = body?.object?.user?.connections?.find((c) => c.platform === "KICK")?.username; @@ -1929,7 +1955,13 @@ const useChatStore = create((set, get) => ({ chatroomId, }, ); - useCosmeticsStore?.getState()?.addUserStyle(transformedUsername, body); + const addUserStyle = useCosmeticsStore?.getState()?.addUserStyle; + if (addUserStyle) { + console.log(`[ChatProvider] Calling CosmeticsStore.addUserStyle for ${transformedUsername}`); + addUserStyle(transformedUsername, body); + } else { + console.error(`[ChatProvider] CosmeticsStore.addUserStyle method not available!`); + } break; } default: @@ -2169,7 +2201,9 @@ const useChatStore = create((set, get) => ({ get().connectToChatroom(newChatroom); // Connect to 7TV WebSocket - get().connectToStvWebSocket(newChatroom); + // DISABLED: Using shared connection system via connectionManager.initializeConnections + console.log(`[ChatProvider] Skipping individual 7TV connection for new chatroom ${newChatroom.id} - using shared connection system`); + // get().connectToStvWebSocket(newChatroom); // Save to local storage localStorage.setItem("chatrooms", JSON.stringify([...savedChatrooms, newChatroom])); @@ -4055,7 +4089,9 @@ if (window.location.pathname === "/" || window.location.pathname.endsWith("index if (chatrooms?.length === 0) return; chatrooms.forEach((chatroom) => { - console.log("[7tv Presence]: Sending presence check for chatroom:", chatroom.streamerData.user_id); + if (window.__KT_TELEMETRY_UTILS__?.shouldLogDebug?.()) { + console.log("[7tv Presence]: Sending presence check for chatroom:", chatroom.streamerData.user_id); + } useChatStore.getState().sendPresenceUpdate(storeStvId, chatroom.streamerData.user_id); }); }, diff --git a/src/renderer/src/providers/CosmeticsProvider.jsx b/src/renderer/src/providers/CosmeticsProvider.jsx index 15d42e7..705b5e9 100644 --- a/src/renderer/src/providers/CosmeticsProvider.jsx +++ b/src/renderer/src/providers/CosmeticsProvider.jsx @@ -8,16 +8,38 @@ const useCosmeticsStore = create((set, get) => ({ }, addUserStyle: async (username, body) => { - if (!body?.object?.user?.style) return; + console.log(`[CosmeticsStore DIAGNOSTIC] addUserStyle called`, { + username, + hasBody: !!body, + hasUserStyle: !!body?.object?.user?.style, + objectKind: body?.object?.kind, + bodyKeys: Object.keys(body || {}), + objectKeys: Object.keys(body?.object || {}), + userKeys: Object.keys(body?.object?.user || {}), + fullBody: body + }); + + if (!body?.object?.user?.style) { + console.log(`[CosmeticsStore DIAGNOSTIC] No user style found in body, returning early`, { + hasObject: !!body?.object, + hasUser: !!body?.object?.user, + hasStyle: !!body?.object?.user?.style, + userObject: body?.object?.user + }); + return; + } + const transformedUsername = username.toLowerCase(); const userStyle = body.object.user; console.log( - `[CosmeticsStore] Upserting style for ${transformedUsername}`, + `[CosmeticsStore DIAGNOSTIC] Upserting style for ${transformedUsername}`, { badgeId: userStyle?.style?.badge_id, paintId: userStyle?.style?.paint_id, entitlementId: body?.object?.id, + userStyleObject: userStyle?.style, + connections: userStyle?.connections }, ); @@ -45,14 +67,40 @@ const useCosmeticsStore = create((set, get) => ({ }, getUserStyle: (username) => { - if (!username) return null; + if (!username) { + console.log(`[CosmeticsStore DIAGNOSTIC] getUserStyle called with empty username`); + return null; + } + const transformedUsername = username.toLowerCase(); const userStyle = get().userStyles[transformedUsername]; + const globalCosmetics = get().globalCosmetics; + + console.log(`[CosmeticsStore DIAGNOSTIC] getUserStyle for ${transformedUsername}`, { + hasUserStyle: !!userStyle, + userStyleBadgeId: userStyle?.badgeId, + userStylePaintId: userStyle?.paintId, + totalBadges: globalCosmetics?.badges?.length, + totalPaints: globalCosmetics?.paints?.length, + userStyleObject: userStyle + }); - if (!userStyle?.badgeId && !userStyle?.paintId) return null; - - const badge = get().globalCosmetics?.badges?.find((b) => b.id === userStyle.badgeId); - const paint = get().globalCosmetics?.paints?.find((p) => p.id === userStyle.paintId); + if (!userStyle?.badgeId && !userStyle?.paintId) { + console.log(`[CosmeticsStore DIAGNOSTIC] No badge or paint for ${transformedUsername}`); + return null; + } + + const badge = globalCosmetics?.badges?.find((b) => b.id === userStyle.badgeId); + const paint = globalCosmetics?.paints?.find((p) => p.id === userStyle.paintId); + + console.log(`[CosmeticsStore DIAGNOSTIC] Found cosmetics for ${transformedUsername}`, { + foundBadge: !!badge, + foundPaint: !!paint, + badgeId: userStyle.badgeId, + paintId: userStyle.paintId, + badge: badge, + paint: paint + }); return { badge, @@ -63,21 +111,35 @@ const useCosmeticsStore = create((set, get) => ({ }, addCosmetics: (body) => { - console.log( - `[CosmeticsStore] Updating global cosmetics catalog`, - { - badges: body?.badges?.length, - paints: body?.paints?.length, - }, - ); + console.log(`[CosmeticsStore DIAGNOSTIC] addCosmetics called`, { + hasBody: !!body, + badges: body?.badges?.length, + paints: body?.paints?.length, + bodyKeys: Object.keys(body || {}), + firstBadge: body?.badges?.[0], + firstPaint: body?.paints?.[0] + }); - set(() => { + const currentState = get(); + console.log(`[CosmeticsStore DIAGNOSTIC] Current state before update`, { + currentBadges: currentState.globalCosmetics?.badges?.length, + currentPaints: currentState.globalCosmetics?.paints?.length + }); + + set((state) => { const newState = { globalCosmetics: { ...body, }, }; + console.log(`[CosmeticsStore DIAGNOSTIC] State updated`, { + newBadges: newState.globalCosmetics?.badges?.length, + newPaints: newState.globalCosmetics?.paints?.length, + previousBadges: state.globalCosmetics?.badges?.length, + previousPaints: state.globalCosmetics?.paints?.length + }); + return newState; }); }, @@ -99,6 +161,79 @@ const useCosmeticsStore = create((set, get) => ({ return get().globalCosmetics[userStyle.paintId]; }, + + // TEST FUNCTION: Simulate receiving cosmetics to verify the store works + testCosmetics: () => { + console.log(`[CosmeticsStore TEST] Simulating cosmetic events...`); + + // Simulate cosmetic.create event + const testCosmetics = { + badges: [ + { + id: "test-badge-1", + title: "Test Badge", + url: "https://cdn.7tv.app/badge/test/1x.webp" + } + ], + paints: [ + { + id: "test-paint-1", + name: "Test Paint", + backgroundImage: "linear-gradient(45deg, #ff0000, #00ff00)", + KIND: "non-animated" + } + ] + }; + + get().addCosmetics(testCosmetics); + + // Simulate entitlement.create event + const testEntitlement = { + object: { + kind: "ENTITLEMENT", + id: "test-entitlement-1", + user: { + id: "test-user-id", + username: "testuser", + style: { + badge_id: "test-badge-1", + paint_id: "test-paint-1", + color: -1 + }, + connections: [ + { + platform: "KICK", + username: "testuser" + } + ] + } + } + }; + + get().addUserStyle("testuser", testEntitlement); + + console.log(`[CosmeticsStore TEST] Test complete. Try getUserStyle('testuser')`); + }, })); +// Expose test function globally for debugging +if (typeof window !== 'undefined') { + window.testCosmeticsStore = () => { + console.log(`[TEST] Starting cosmetics test...`); + const store = useCosmeticsStore.getState(); + console.log(`[TEST] Got store:`, !!store); + store.testCosmetics(); + + // Also test retrieval + setTimeout(() => { + const result = store.getUserStyle('testuser'); + console.log(`[CosmeticsStore TEST] getUserStyle result:`, result); + }, 100); + }; + + // Also expose the store itself for direct testing + window.cosmeticsStore = useCosmeticsStore; + console.log(`[CosmeticsProvider] Exposed window.testCosmeticsStore() and window.cosmeticsStore`); +} + export default useCosmeticsStore; diff --git a/utils/services/seventv/sharedStvWebSocket.js b/utils/services/seventv/sharedStvWebSocket.js index 9d9d30f..40831c2 100644 --- a/utils/services/seventv/sharedStvWebSocket.js +++ b/utils/services/seventv/sharedStvWebSocket.js @@ -344,10 +344,25 @@ class SharedStvWebSocket extends EventTarget { await this.delay(1000); // Subscribe to events for all chatrooms + console.log(`[Shared7TV]: Starting subscription to all events for ${this.chatrooms.size} chatrooms`); + try { + window.app?.telemetry?.recordWebSocketEvent?.('7tv_shared', 'subscription_start', { + chatroom_count: this.chatrooms.size + }); + } catch (_) {} await this.subscribeToAllEvents(); + console.log(`[Shared7TV]: Finished subscribing to all events`); + try { + window.app?.telemetry?.recordWebSocketEvent?.('7tv_shared', 'subscription_complete', { + chatroom_count: this.chatrooms.size, + subscribed_events: this.subscribedEvents.size + }); + } catch (_) {} // Setup message handler + console.log(`[Shared7TV]: Setting up message handler`); this.setupMessageHandler(); + console.log(`[Shared7TV]: Message handler setup complete`); // Dispatch connection event this.dispatchEvent( @@ -365,11 +380,22 @@ class SharedStvWebSocket extends EventTarget { handleConnectionError() { this.reconnectAttempts++; console.log(`[Shared7TV]: Connection error. Attempt ${this.reconnectAttempts}`); + try { + window.app?.telemetry?.recordWebSocketError?.('7tv_shared', 'connection_error', { + attempt: this.reconnectAttempts, + chatroom_count: this.chatrooms.size + }); + } catch (_) {} } handleReconnection() { if (!this.shouldReconnect) { console.log(`[Shared7TV]: Reconnection disabled`); + try { + window.app?.telemetry?.recordWebSocketEvent?.('7tv_shared', 'reconnection_disabled', { + attempt: this.reconnectAttempts + }); + } catch (_) {} return; } @@ -379,6 +405,13 @@ class SharedStvWebSocket extends EventTarget { const delay = this.startDelay * Math.pow(2, step - 1); console.log(`[Shared7TV]: Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`); + try { + window.app?.telemetry?.recordWebSocketEvent?.('7tv_shared', 'reconnection_scheduled', { + attempt: this.reconnectAttempts + 1, + delay_ms: delay, + step: step + }); + } catch (_) {} setTimeout(() => { this.connect(); @@ -475,8 +508,10 @@ class SharedStvWebSocket extends EventTarget { * Subscribe to cosmetic events for a specific chatroom */ async subscribeToCosmeticEvents(chatroomId, channelKickID) { + console.log(`[Shared7TV]: Attempting to subscribe to cosmetic events for chatroom ${chatroomId}, channelKickID: ${channelKickID}`); + if (!this.chat || this.chat.readyState !== WebSocket.OPEN) { - console.log(`[Shared7TV]: Cannot subscribe to cosmetic events - WebSocket not ready`); + console.log(`[Shared7TV]: Cannot subscribe to cosmetic events - WebSocket not ready. State: ${this.chat?.readyState}`); return; } @@ -497,6 +532,7 @@ class SharedStvWebSocket extends EventTarget { }, }; + console.log(`[Shared7TV]: Sending cosmetic subscription message:`, subscribeAllCosmetics); this.chat.send(JSON.stringify(subscribeAllCosmetics)); this.subscribedEvents.add(eventKey); console.log(`[Shared7TV]: Subscribed to cosmetic.* events for chatroom ${chatroomId}`); @@ -506,8 +542,10 @@ class SharedStvWebSocket extends EventTarget { * Subscribe to entitlement events for a specific chatroom */ async subscribeToEntitlementEvents(chatroomId, channelKickID) { + console.log(`[Shared7TV]: Attempting to subscribe to entitlement events for chatroom ${chatroomId}, channelKickID: ${channelKickID}`); + if (!this.chat || this.chat.readyState !== WebSocket.OPEN) { - console.log(`[Shared7TV]: Cannot subscribe to entitlement events - WebSocket not ready`); + console.log(`[Shared7TV]: Cannot subscribe to entitlement events - WebSocket not ready. State: ${this.chat?.readyState}`); return; } @@ -528,6 +566,7 @@ class SharedStvWebSocket extends EventTarget { }, }; + console.log(`[Shared7TV]: Sending entitlement subscription message:`, subscribeAllEntitlements); this.chat.send(JSON.stringify(subscribeAllEntitlements)); this.subscribedEvents.add(eventKey); console.log(`[Shared7TV]: Subscribed to entitlement.* events for chatroom ${chatroomId}`); @@ -579,13 +618,153 @@ class SharedStvWebSocket extends EventTarget { setupMessageHandler() { this.chat.onmessage = (event) => { + console.log(`[Shared7TV]: Raw WebSocket message received:`, event.data); try { const msg = JSON.parse(event.data); + console.log(`[Shared7TV]: Parsed message:`, { + op: msg?.op, + type: msg?.d?.type, + hasBody: !!msg?.d?.body, + fullMessage: msg + }); - if (!msg?.d?.body) return; + // Handle different 7TV opcodes + switch (msg?.op) { + case 0: // Dispatch (actual events) + console.log(`[Shared7TV]: Dispatch event received`, { type: msg?.d?.type }); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'dispatch', msg?.d?.type); + } catch (_) {} + break; + case 1: // Hello (connection established) + console.log(`[Shared7TV]: Hello received`, { + heartbeat_interval: msg?.d?.heartbeat_interval, + session_id: msg?.d?.session_id + }); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'hello', null, { + heartbeat_interval: msg?.d?.heartbeat_interval, + has_session_id: !!msg?.d?.session_id + }); + } catch (_) {} + return; + case 2: // Heartbeat + console.log(`[Shared7TV]: Heartbeat received`, { count: msg?.d?.count }); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'heartbeat', null, { + count: msg?.d?.count + }); + } catch (_) {} + return; // Don't process heartbeats further + case 4: // Reconnect (server requests reconnection) + console.log(`[Shared7TV]: Reconnect request received`, msg?.d); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'reconnect', null, { + reason: msg?.d?.reason + }); + } catch (_) {} + // Handle reconnection logic if needed + return; + case 5: // Ack (acknowledgment) + console.log(`[Shared7TV]: ACK received`, { + command: msg?.d?.command, + type: msg?.d?.data?.type, + id: msg?.d?.data?.id + }); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'ack', msg?.d?.data?.type, { + command: msg?.d?.command + }); + } catch (_) {} + return; + case 6: // Error + console.error(`[Shared7TV]: Error received`, { + message: msg?.d?.message, + code: msg?.d?.code, + data: msg?.d?.data + }); + try { + window.app?.telemetry?.recordWebSocketError?.('7tv_shared', msg?.d?.message, { + code: msg?.d?.code, + data: msg?.d?.data + }); + } catch (_) {} + return; + case 7: // EndOfStream + console.log(`[Shared7TV]: End of stream received`, msg?.d); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'end_of_stream', null, msg?.d); + } catch (_) {} + return; + case 33: // Identify (client authentication) + console.log(`[Shared7TV]: Identify opcode (should not be received by client)`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'identify_unexpected', null); + } catch (_) {} + return; + case 34: // Resume (session resumption) + console.log(`[Shared7TV]: Resume opcode (should not be received by client)`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'resume_unexpected', null); + } catch (_) {} + return; + case 35: // Subscribe (subscription request) + console.log(`[Shared7TV]: Subscribe opcode (should not be received by client)`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'subscribe_unexpected', null); + } catch (_) {} + return; + case 36: // Unsubscribe (unsubscription request) + console.log(`[Shared7TV]: Unsubscribe opcode (should not be received by client)`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'unsubscribe_unexpected', null); + } catch (_) {} + return; + case 37: // Signal + console.log(`[Shared7TV]: Signal received`, msg?.d); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'signal', null, msg?.d); + } catch (_) {} + return; + case 38: // Bridge (deprecated) + console.log(`[Shared7TV]: Bridge opcode received (deprecated)`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'bridge_deprecated', null); + } catch (_) {} + return; + default: + console.log(`[Shared7TV]: Unknown opcode ${msg?.op}:`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'unknown_opcode', null, { + opcode: msg?.op + }); + } catch (_) {} + return; + } + + // Only process dispatch events (op: 0) that have a body + if (!msg?.d?.body) { + console.log(`[Shared7TV]: Dispatch event has no body, skipping:`, msg); + return; + } const { body, type } = msg.d; + // DIAGNOSTIC: Log all cosmetic-related events with detailed info + if (type?.includes('cosmetic') || type?.includes('entitlement')) { + console.log(`[Shared7TV DIAGNOSTIC]: Cosmetic event received`, { + type: type, + objectKind: body?.object?.kind, + objectId: body?.object?.id, + userId: body?.object?.user?.id, + username: body?.object?.user?.username, + platform: body?.context?.platform || body?.condition?.platform, + channelId: body?.context?.id || body?.condition?.id, + hasData: !!body?.object?.data, + fullBody: body + }); + } + // Find which chatroom this event belongs to const chatroomId = this.findChatroomForEvent(body, type); @@ -599,6 +778,7 @@ class SharedStvWebSocket extends EventTarget { switch (type) { case "user.update": + console.log(`[Shared7TV]: Dispatching user.update event for chatroomId: ${chatroomId}`); this.dispatchEvent( new CustomEvent("message", { detail: { @@ -611,6 +791,7 @@ class SharedStvWebSocket extends EventTarget { break; case "emote_set.update": + console.log(`[Shared7TV]: Dispatching emote_set.update event for chatroomId: ${chatroomId}`); this.dispatchEvent( new CustomEvent("message", { detail: { @@ -633,6 +814,11 @@ class SharedStvWebSocket extends EventTarget { }, ); + console.log(`[Shared7TV]: Dispatching cosmetic.create event for chatroomId: ${chatroomId}`, { + badgeCount: cosmetics?.badges?.length, + paintCount: cosmetics?.paints?.length, + eventDetail: { chatroomId, type: "cosmetic.create" } + }); this.dispatchEvent( new CustomEvent("message", { detail: { @@ -654,6 +840,12 @@ class SharedStvWebSocket extends EventTarget { paintId: body?.object?.user?.style?.paint_id, }, ); + console.log(`[Shared7TV]: Dispatching entitlement.create event for chatroomId: ${chatroomId}`, { + username: body?.object?.user?.username, + badgeId: body?.object?.user?.style?.badge_id, + paintId: body?.object?.user?.style?.paint_id, + eventDetail: { chatroomId, type: "entitlement.create" } + }); this.dispatchEvent( new CustomEvent("message", { detail: { diff --git a/utils/services/seventv/stvWebsocket.js b/utils/services/seventv/stvWebsocket.js index d3b2779..20d71c8 100644 --- a/utils/services/seventv/stvWebsocket.js +++ b/utils/services/seventv/stvWebsocket.js @@ -218,11 +218,23 @@ class StvWebSocket extends EventTarget { handleConnectionError() { this.reconnectAttempts++; console.log(`[7TV]: Connection error. Attempt ${this.reconnectAttempts}`); + try { + window.app?.telemetry?.recordWebSocketError?.(`7tv_${this.channelKickID}`, 'connection_error', { + attempt: this.reconnectAttempts, + channelKickID: this.channelKickID + }); + } catch (_) {} } handleReconnection() { if (!this.shouldReconnect) { console.log(`[7TV]: Reconnection disabled for chatroom ${this.channelKickID}`); + try { + window.app?.telemetry?.recordWebSocketEvent?.(`7tv_${this.channelKickID}`, 'reconnection_disabled', { + attempt: this.reconnectAttempts, + channelKickID: this.channelKickID + }); + } catch (_) {} return; } @@ -232,6 +244,14 @@ class StvWebSocket extends EventTarget { const delay = this.startDelay * Math.pow(2, step - 1); console.log(`[7TV]: Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); + try { + window.app?.telemetry?.recordWebSocketEvent?.(`7tv_${this.channelKickID}`, 'reconnection_scheduled', { + attempt: this.reconnectAttempts, + delay_ms: delay, + step: step, + channelKickID: this.channelKickID + }); + } catch (_) {} setTimeout(() => { this.connect(); @@ -332,8 +352,120 @@ class StvWebSocket extends EventTarget { try { const msg = JSON.parse(event.data); - // Log ALL messages to see if we're getting any at all + // Handle different 7TV opcodes + switch (msg?.op) { + case 0: // Dispatch (actual events) + console.log(`[7TV]: Dispatch event received for channel ${this.channelKickID}`, { type: msg?.d?.type }); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'dispatch', msg?.d?.type); + } catch (_) {} + break; + case 1: // Hello (connection established) + console.log(`[7TV]: Hello received for channel ${this.channelKickID}`, { + heartbeat_interval: msg?.d?.heartbeat_interval, + session_id: msg?.d?.session_id + }); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'hello', null, { + heartbeat_interval: msg?.d?.heartbeat_interval, + has_session_id: !!msg?.d?.session_id + }); + } catch (_) {} + return; + case 2: // Heartbeat + console.log(`[7TV]: Heartbeat received for channel ${this.channelKickID}`, { count: msg?.d?.count }); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'heartbeat', null, { + count: msg?.d?.count + }); + } catch (_) {} + return; // Don't process heartbeats further + case 4: // Reconnect (server requests reconnection) + console.log(`[7TV]: Reconnect request received for channel ${this.channelKickID}`, msg?.d); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'reconnect', null, { + reason: msg?.d?.reason + }); + } catch (_) {} + return; + case 5: // Ack (acknowledgment) + console.log(`[7TV]: ACK received for channel ${this.channelKickID}`, { + command: msg?.d?.command, + type: msg?.d?.data?.type, + id: msg?.d?.data?.id + }); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'ack', msg?.d?.data?.type, { + command: msg?.d?.command + }); + } catch (_) {} + return; + case 6: // Error + console.error(`[7TV]: Error received for channel ${this.channelKickID}`, { + message: msg?.d?.message, + code: msg?.d?.code, + data: msg?.d?.data + }); + try { + window.app?.telemetry?.recordWebSocketError?.(`7tv_${this.channelKickID}`, msg?.d?.message, { + code: msg?.d?.code, + data: msg?.d?.data + }); + } catch (_) {} + return; + case 7: // EndOfStream + console.log(`[7TV]: End of stream received for channel ${this.channelKickID}`, msg?.d); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'end_of_stream', null, msg?.d); + } catch (_) {} + return; + case 33: // Identify (client authentication) + console.log(`[7TV]: Identify opcode (should not be received by client) for channel ${this.channelKickID}`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'identify_unexpected', null); + } catch (_) {} + return; + case 34: // Resume (session resumption) + console.log(`[7TV]: Resume opcode (should not be received by client) for channel ${this.channelKickID}`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'resume_unexpected', null); + } catch (_) {} + return; + case 35: // Subscribe (subscription request) + console.log(`[7TV]: Subscribe opcode (should not be received by client) for channel ${this.channelKickID}`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'subscribe_unexpected', null); + } catch (_) {} + return; + case 36: // Unsubscribe (unsubscription request) + console.log(`[7TV]: Unsubscribe opcode (should not be received by client) for channel ${this.channelKickID}`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'unsubscribe_unexpected', null); + } catch (_) {} + return; + case 37: // Signal + console.log(`[7TV]: Signal received for channel ${this.channelKickID}`, msg?.d); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'signal', null, msg?.d); + } catch (_) {} + return; + case 38: // Bridge (deprecated) + console.log(`[7TV]: Bridge opcode received (deprecated) for channel ${this.channelKickID}`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'bridge_deprecated', null); + } catch (_) {} + return; + default: + console.log(`[7TV]: Unknown opcode ${msg?.op} for channel ${this.channelKickID}:`, msg); + try { + window.app?.telemetry?.recordWebSocketMessage?.(`7tv_${this.channelKickID}`, 'unknown_opcode', null, { + opcode: msg?.op + }); + } catch (_) {} + return; + } + // Only process dispatch events (op: 0) that have a body if (!msg?.d?.body) { return; } From 4d923b52eee362f878283189ff905442e29d18dd Mon Sep 17 00:00:00 2001 From: BP602 <3460479+BP602@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:31:49 +0200 Subject: [PATCH 3/5] Update src/renderer/src/providers/ChatProvider.jsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/renderer/src/providers/ChatProvider.jsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/providers/ChatProvider.jsx b/src/renderer/src/providers/ChatProvider.jsx index fae0927..79896ab 100644 --- a/src/renderer/src/providers/ChatProvider.jsx +++ b/src/renderer/src/providers/ChatProvider.jsx @@ -1926,6 +1926,7 @@ const useChatStore = create((set, get) => ({ get().handleEmoteSetUpdate(chatroomId, body); break; case "cosmetic.create": + case "cosmetic.create": { console.log( `[ChatProvider] Applying cosmetic catalog update for ${chatroomId ?? 'all chatrooms'}`, { @@ -1933,17 +1934,19 @@ const useChatStore = create((set, get) => ({ paints: body?.paints?.length, }, ); - const cosmetics = useCosmeticsStore?.getState()?.addCosmetics; - if (cosmetics) { - console.log(`[ChatProvider] Calling CosmeticsStore.addCosmetics with body:`, { + const addCosmetics = useCosmeticsStore?.getState()?.addCosmetics; + if (addCosmetics) { + if (window.__KT_TELEMETRY_UTILS__?.shouldLogDebug?.()) console.log(`[ChatProvider] Calling CosmeticsStore.addCosmetics with body:`, { badges: body?.badges?.length, paints: body?.paints?.length }); - cosmetics(body); + addCosmetics(body); } else { console.error(`[ChatProvider] CosmeticsStore.addCosmetics method not available!`); } break; + } + break; case "entitlement.create": { const username = body?.object?.user?.connections?.find((c) => c.platform === "KICK")?.username; const transformedUsername = username?.replaceAll("-", "_").toLowerCase(); From a63ec74f258a10c9078363b2379bb72d82c2c981 Mon Sep 17 00:00:00 2001 From: BP602 Date: Mon, 22 Sep 2025 01:22:40 +0200 Subject: [PATCH 4/5] feat(telemetry): add consolidated VITE flags and OTEL debug logs - Introduce getTelemetryLevel/getTelemetryDebug that prefer VITE_TELEMETRY_* and fall back to MAIN_/RENDERER_ variants for cross-process consistency - Add shouldLogDebug and gate verbose logs on debug=true or level=VERBOSE - Add guarded request/response/result logs to the main OTEL IPC relay for trace exports - Expose telemetry utils (level/debug/shouldLogDebug) to window in the renderer - Defaults remain NORMAL/false to avoid noise and simplify troubleshooting --- src/main/index.js | 45 +++++- src/renderer/src/telemetry/webTracing.js | 189 +++++++++++++---------- src/telemetry/user-analytics.js | 84 +++++++--- utils/services/connectionManager.js | 5 + 4 files changed, 217 insertions(+), 106 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 760f26c..c268edd 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -133,6 +133,33 @@ try { console.warn('[Telemetry]: Failed to set service.version from package/app version:', e?.message || e); } +// Telemetry debug configuration for main process +const getTelemetryDebug = () => { + try { + // Check consolidated VITE_ vars first (available to all processes), then fall back to old vars + return import.meta.env.VITE_TELEMETRY_DEBUG === 'true' || + import.meta.env.MAIN_VITE_TELEMETRY_DEBUG === 'true'; + } catch { + return false; + } +}; + +const getTelemetryLevel = () => { + try { + // Check consolidated VITE_ vars first (available to all processes), then fall back to old vars + const level = import.meta.env.VITE_TELEMETRY_LEVEL || + import.meta.env.MAIN_VITE_TELEMETRY_LEVEL || + 'NORMAL'; + return level.toUpperCase(); + } catch { + return 'NORMAL'; + } +}; + +const shouldLogDebug = () => { + return getTelemetryDebug() || getTelemetryLevel() === 'VERBOSE'; +}; + // Load metrics with fallback let metrics = null; try { @@ -686,8 +713,10 @@ ipcMain.handle("otel:trace-export-json", async (_e, exportJson) => { const startedAt = Date.now(); try { - console.log(`[OTEL IPC Relay][${requestId}] Received trace export from renderer`); - console.log(`[OTEL IPC Relay][${requestId}] Payload size: ${JSON.stringify(exportJson || {}).length} chars`); + if (shouldLogDebug()) { + console.log(`[OTEL IPC Relay][${requestId}] Received trace export from renderer`); + console.log(`[OTEL IPC Relay][${requestId}] Payload size: ${JSON.stringify(exportJson || {}).length} chars`); + } const base = import.meta.env.MAIN_VITE_OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || ""; const endpoint = import.meta.env.MAIN_VITE_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || @@ -732,7 +761,9 @@ ipcMain.handle("otel:trace-export-json", async (_e, exportJson) => { timeout: 15000, }; - console.log(`[OTEL IPC Relay][${requestId}] → POST ${url.hostname}${options.path}`); + if (shouldLogDebug()) { + console.log(`[OTEL IPC Relay][${requestId}] → POST ${url.hostname}${options.path}`); + } const result = await new Promise((resolve, reject) => { const req = https.request(options, (res) => { @@ -742,7 +773,9 @@ ipcMain.handle("otel:trace-export-json", async (_e, exportJson) => { const ms = Date.now() - startedAt; const responseBody = Buffer.concat(chunks).toString("utf8"); - console.log(`[OTEL IPC Relay][${requestId}] ← ${res.statusCode} (${ms}ms)`); + if (shouldLogDebug()) { + console.log(`[OTEL IPC Relay][${requestId}] ← ${res.statusCode} (${ms}ms)`); + } resolve({ statusCode: res.statusCode || 0, responseBody }); }); }); @@ -762,7 +795,9 @@ ipcMain.handle("otel:trace-export-json", async (_e, exportJson) => { }); const success = result.statusCode >= 200 && result.statusCode < 300; - console.log(`[OTEL IPC Relay][${requestId}] Result: ${success ? 'success' : 'failed'}`); + if (shouldLogDebug()) { + console.log(`[OTEL IPC Relay][${requestId}] Result: ${success ? 'success' : 'failed'}`); + } return { ok: success, status: result.statusCode, requestId }; } catch (e) { diff --git a/src/renderer/src/telemetry/webTracing.js b/src/renderer/src/telemetry/webTracing.js index 770ecdf..c81276f 100644 --- a/src/renderer/src/telemetry/webTracing.js +++ b/src/renderer/src/telemetry/webTracing.js @@ -198,13 +198,30 @@ if (!telemetryEnabled && typeof window !== 'undefined') { // Telemetry Level Configuration and Sampling Utilities const getTelemetryLevel = () => { try { - const level = import.meta.env.RENDERER_VITE_TELEMETRY_LEVEL || 'NORMAL'; + // Check consolidated VITE_ vars first (available to all processes), then fall back to old vars + const level = import.meta.env.VITE_TELEMETRY_LEVEL || + import.meta.env.RENDERER_VITE_TELEMETRY_LEVEL || + 'NORMAL'; return level.toUpperCase(); } catch { return 'NORMAL'; } }; +const getTelemetryDebug = () => { + try { + // Check consolidated VITE_ vars first (available to all processes), then fall back to old vars + return import.meta.env.VITE_TELEMETRY_DEBUG === 'true' || + import.meta.env.RENDERER_VITE_TELEMETRY_DEBUG === 'true'; + } catch { + return false; + } +}; + +const shouldLogDebug = () => { + return getTelemetryDebug() || getTelemetryLevel() === 'VERBOSE'; +}; + const TELEMETRY_LEVELS = { MINIMAL: { priority: 1, @@ -291,6 +308,8 @@ const checkStartupPhase = () => { // Export utilities for use in components window.__KT_TELEMETRY_UTILS__ = { getTelemetryLevel, + getTelemetryDebug, + shouldLogDebug, shouldEmitTelemetry, shouldSampleMessageParser, shouldEmitLexicalUpdate, @@ -654,45 +673,51 @@ if (!window.__KT_RENDERER_OTEL_INITIALIZED__ && telemetryEnabled) { } catch {} } - console.log(`[Renderer OTEL][${exportId}] IPCSpanExporter.export() called:`, { - exportId, - exportCount: this.exportCount, - spanCount: spanArray.length, - serviceName: this.serviceName, - deploymentEnv: this.deploymentEnv, - traceIds: traceInfo.traceIds, - spanIds: traceInfo.spanIds.slice(0, 3), // First 3 span IDs - spanNames: traceInfo.spanNames, - parentSpanIds: traceInfo.parentSpanIds.slice(0, 3) - }); + if (shouldLogDebug()) { + console.log(`[Renderer OTEL][${exportId}] IPCSpanExporter.export() called:`, { + exportId, + exportCount: this.exportCount, + spanCount: spanArray.length, + serviceName: this.serviceName, + deploymentEnv: this.deploymentEnv, + traceIds: traceInfo.traceIds, + spanIds: traceInfo.spanIds.slice(0, 3), // First 3 span IDs + spanNames: traceInfo.spanNames, + parentSpanIds: traceInfo.parentSpanIds.slice(0, 3) + }); + } const req = this._toOtlpJson(spans); const reqSize = JSON.stringify(req).length; - console.log(`[Renderer OTEL][${exportId}] Converted to OTLP JSON:`, { - exportId, - requestSize: reqSize, - resourceSpansCount: req.resourceSpans?.length || 0, - traceIds: traceInfo.traceIds, - actualTraceIds: traceInfo.traceIds, - traceIdLengths: traceInfo.traceIds.map(id => id?.length || 0) - }); + if (shouldLogDebug()) { + console.log(`[Renderer OTEL][${exportId}] Converted to OTLP JSON:`, { + exportId, + requestSize: reqSize, + resourceSpansCount: req.resourceSpans?.length || 0, + traceIds: traceInfo.traceIds, + actualTraceIds: traceInfo.traceIds, + traceIdLengths: traceInfo.traceIds.map(id => id?.length || 0) + }); + } const res = await window.telemetry.exportTracesJson(req); const duration = performance.now() - startTime; const ok = !!res?.ok && (!res.status || (res.status >= 200 && res.status < 300)); - console.log(`[Renderer OTEL][${exportId}] IPC export result:`, { - exportId, - success: ok, - duration: `${Math.round(duration)}ms`, - responseStatus: res?.status, - responseOk: res?.ok, - requestId: res?.requestId, - traceIds: traceInfo.traceIds, - returnedTraceIds: res?.traceIds - }); + if (shouldLogDebug()) { + console.log(`[Renderer OTEL][${exportId}] IPC export result:`, { + exportId, + success: ok, + duration: `${Math.round(duration)}ms`, + responseStatus: res?.status, + responseOk: res?.ok, + requestId: res?.requestId, + traceIds: traceInfo.traceIds, + returnedTraceIds: res?.traceIds + }); + } resultCallback({ code: ok ? 0 : 1 }); } catch (e) { @@ -883,50 +908,52 @@ if (!window.__KT_RENDERER_OTEL_INITIALIZED__ && telemetryEnabled) { const directConversion = rawSec * 1000000000n + rawNs; const directDate = new Date(Number(directConversion) / 1e6); - console.log(`[Renderer OTEL] Span validation debug:`, { - traceId, - traceIdHexLength: traceId.length, - spanId, - spanIdHexLength: spanId.length, - parentSpanId, - parentSpanIdHexLength: parentSpanId.length, - // For visibility when debugging, also show base64 versions - traceIdBase64: hexToBase64(traceIdHex), - spanIdBase64: hexToBase64(spanIdHex), - parentSpanIdBase64: parentSpanIdHex ? hexToBase64(parentSpanIdHex) : '', - startTimeUnixNano: startNs.toString(), - endTimeUnixNano: endNs.toString(), - startDate: startDate.toISOString(), - endDate: endDate.toISOString(), - startYear: startDate.getFullYear(), - endYear: endDate.getFullYear(), - isStartTimeReasonable: startDate.getFullYear() >= 2020 && startDate.getFullYear() <= 2030, - isEndTimeReasonable: endDate.getFullYear() >= 2020 && endDate.getFullYear() <= 2030, - duration: (endNs - startNs).toString() + ' nanos', - durationMs: Number(endNs - startNs) / 1e6, - kind: Number(s.kind) || 0, - name: s.name || 'span', - nowMs, - nowNs: nowNs.toString(), - timeDiffFromNow: Number(nowNs - startNs), - statusCode: s.status?.code, - statusMessage: s.status?.message, - statusWillBeIncluded: !!(s.status?.code && s.status.code > 0), - statusExists: !!s.status, - statusCodeType: typeof s.status?.code, - // Raw hrtime debugging - rawStartTime: s.startTime, - rawEndTime: s.endTime, - performanceTimeOrigin: performance?.timeOrigin, - performanceNow: performance?.now?.(), - // Step-by-step conversion debugging - rawSecBigInt: rawSec.toString(), - rawNsBigInt: rawNs.toString(), - directConversion: directConversion.toString(), - directDate: directDate.toISOString(), - directYear: directDate.getFullYear(), - directVsCalculated: directConversion.toString() === startNs.toString() - }); + if (shouldLogDebug()) { + console.log(`[Renderer OTEL] Span validation debug:`, { + traceId, + traceIdHexLength: traceId.length, + spanId, + spanIdHexLength: spanId.length, + parentSpanId, + parentSpanIdHexLength: parentSpanId.length, + // For visibility when debugging, also show base64 versions + traceIdBase64: hexToBase64(traceIdHex), + spanIdBase64: hexToBase64(spanIdHex), + parentSpanIdBase64: parentSpanIdHex ? hexToBase64(parentSpanIdHex) : '', + startTimeUnixNano: startNs.toString(), + endTimeUnixNano: endNs.toString(), + startDate: startDate.toISOString(), + endDate: endDate.toISOString(), + startYear: startDate.getFullYear(), + endYear: endDate.getFullYear(), + isStartTimeReasonable: startDate.getFullYear() >= 2020 && startDate.getFullYear() <= 2030, + isEndTimeReasonable: endDate.getFullYear() >= 2020 && endDate.getFullYear() <= 2030, + duration: (endNs - startNs).toString() + ' nanos', + durationMs: Number(endNs - startNs) / 1e6, + kind: Number(s.kind) || 0, + name: s.name || 'span', + nowMs, + nowNs: nowNs.toString(), + timeDiffFromNow: Number(nowNs - startNs), + statusCode: s.status?.code, + statusMessage: s.status?.message, + statusWillBeIncluded: !!(s.status?.code && s.status.code > 0), + statusExists: !!s.status, + statusCodeType: typeof s.status?.code, + // Raw hrtime debugging + rawStartTime: s.startTime, + rawEndTime: s.endTime, + performanceTimeOrigin: performance?.timeOrigin, + performanceNow: performance?.now?.(), + // Step-by-step conversion debugging + rawSecBigInt: rawSec.toString(), + rawNsBigInt: rawNs.toString(), + directConversion: directConversion.toString(), + directDate: directDate.toISOString(), + directYear: directDate.getFullYear(), + directVsCalculated: directConversion.toString() === startNs.toString() + }); + } // Warn if timestamps are too far in the future (>5m) or in the past (>24h) try { @@ -969,12 +996,14 @@ if (!window.__KT_RENDERER_OTEL_INITIALIZED__ && telemetryEnabled) { }; } - console.log(`[Renderer OTEL] Status filtering debug:`, { - originalStatusCode: statusCode, - originalStatusMessage: s.status?.message, - isError, - willAddStatus: isError - }); + if (shouldLogDebug()) { + console.log(`[Renderer OTEL] Status filtering debug:`, { + originalStatusCode: statusCode, + originalStatusMessage: s.status?.message, + isError, + willAddStatus: isError + }); + } const spanName = (s.name || 'span'); const spanOut = { diff --git a/src/telemetry/user-analytics.js b/src/telemetry/user-analytics.js index b5c3de4..0ea24b6 100644 --- a/src/telemetry/user-analytics.js +++ b/src/telemetry/user-analytics.js @@ -2,6 +2,34 @@ const { metrics, trace, context } = require('@opentelemetry/api'); const { ErrorMonitor } = require('./error-monitoring'); +// Debug configuration for user analytics +const getTelemetryDebug = () => { + try { + // Check consolidated VITE_ vars first (available to all processes), then fall back to old vars + // Note: In main process, we access these via process.env since they're embedded at build time + return process.env.VITE_TELEMETRY_DEBUG === 'true' || + process.env.MAIN_VITE_TELEMETRY_DEBUG === 'true'; + } catch { + return false; + } +}; + +const getTelemetryLevel = () => { + try { + // Check consolidated VITE_ vars first (available to all processes), then fall back to old vars + const level = process.env.VITE_TELEMETRY_LEVEL || + process.env.MAIN_VITE_TELEMETRY_LEVEL || + 'NORMAL'; + return level.toUpperCase(); + } catch { + return 'NORMAL'; + } +}; + +const shouldLogDebug = () => { + return getTelemetryDebug() || getTelemetryLevel() === 'VERBOSE'; +}; + const pkg = require('../../package.json'); const meter = metrics.getMeter('kicktalk-user-analytics', pkg.version); const tracer = trace.getTracer('kicktalk-user-analytics', pkg.version); @@ -279,7 +307,9 @@ class UserSession { const finalScore = this.calculateFinalSatisfactionScore(); this.satisfactionScore = finalScore; - console.log(`[User Analytics] Session ${this.sessionId} ended: ${this.getSessionDuration()}s, satisfaction: ${finalScore.toFixed(1)}/10`); + if (shouldLogDebug()) { + console.log(`[User Analytics] Session ${this.sessionId} ended: ${this.getSessionDuration()}s, satisfaction: ${finalScore.toFixed(1)}/10`); + } } } @@ -293,7 +323,9 @@ const UserAnalytics = { const session = new UserSession(sessionId, userId); activeSessions.set(sessionId, session); - console.log(`[User Analytics] Started session ${sessionId} for user ${userId || 'anonymous'}`); + if (shouldLogDebug()) { + console.log(`[User Analytics] Started session ${sessionId} for user ${userId || 'anonymous'}`); + } return session; }, @@ -319,7 +351,9 @@ const UserAnalytics = { // Record final satisfaction score const finalScore = session.satisfactionScore; - console.log(`[User Analytics] Session satisfaction: ${finalScore.toFixed(2)}/10`); + if (shouldLogDebug()) { + console.log(`[User Analytics] Session satisfaction: ${finalScore.toFixed(2)}/10`); + } // Store session data for correlation analysis userBehaviorData.set(sessionId, { @@ -417,7 +451,9 @@ const UserAnalytics = { } featureAdoptionData.get(userKey).add(featureName); - console.log(`[User Analytics] Feature usage: ${featureName}.${action} by ${session.userId}`); + if (shouldLogDebug()) { + console.log(`[User Analytics] Feature usage: ${featureName}.${action} by ${session.userId}`); + } }, /** @@ -453,7 +489,9 @@ const UserAnalytics = { timestamp: Date.now() }); - console.log(`[User Analytics] Connection quality: ${quality}/10 (${eventType}) for session ${sessionId}`); + if (shouldLogDebug()) { + console.log(`[User Analytics] Connection quality: ${quality}/10 (${eventType}) for session ${sessionId}`); + } }, /** @@ -586,16 +624,18 @@ const UserAnalytics = { const cleanupDuration = Date.now() - cleanupStartTime; - console.log(`[User Analytics] Cleanup completed in ${cleanupDuration}ms:`, { - active_sessions_cleaned: cleanedSessions, - historical_data_cleaned: cleanedHistoricalData, + if (shouldLogDebug()) { + console.log(`[User Analytics] Cleanup completed in ${cleanupDuration}ms:`, { + active_sessions_cleaned: cleanedSessions, + historical_data_cleaned: cleanedHistoricalData, adoption_data_cleaned: cleanedAdoptionData, performance_data_cleaned: cleanedPerfData, active_sessions_remaining: activeSessions.size, - historical_data_remaining: userBehaviorData.size, - feature_adoption_users: featureAdoptionData.size, - error_impact_sessions: performanceCorrelationData.error_impact_sessions.size - }); + historical_data_remaining: userBehaviorData.size, + feature_adoption_users: featureAdoptionData.size, + error_impact_sessions: performanceCorrelationData.error_impact_sessions.size + }); + } return { cleaned: cleanedSessions + cleanedHistoricalData + cleanedAdoptionData + cleanedPerfData, @@ -633,15 +673,17 @@ const UserAnalytics = { error_impact_sessions: new Set() }; - console.log(`[User Analytics] Force cleanup completed:`, { - before: beforeCounts, - after: { - activeSessions: activeSessions.size, - historicalData: userBehaviorData.size, - featureAdoption: featureAdoptionData.size, - errorImpactSessions: performanceCorrelationData.error_impact_sessions.size - } - }); + if (shouldLogDebug()) { + console.log(`[User Analytics] Force cleanup completed:`, { + before: beforeCounts, + after: { + activeSessions: activeSessions.size, + historicalData: userBehaviorData.size, + featureAdoption: featureAdoptionData.size, + errorImpactSessions: performanceCorrelationData.error_impact_sessions.size + } + }); + } return beforeCounts; }, diff --git a/utils/services/connectionManager.js b/utils/services/connectionManager.js index ff70288..2441b83 100644 --- a/utils/services/connectionManager.js +++ b/utils/services/connectionManager.js @@ -120,12 +120,17 @@ class ConnectionManager { // Set up 7TV event handlers if (handlers.onStvMessage) { + console.log(`[ConnectionManager] Registering onStvMessage handler`); this.stvWebSocket.addEventListener("message", handlers.onStvMessage); + } else { + console.warn(`[ConnectionManager] No onStvMessage handler provided!`); } if (handlers.onStvOpen) { + console.log(`[ConnectionManager] Registering onStvOpen handler`); this.stvWebSocket.addEventListener("open", handlers.onStvOpen); } if (handlers.onStvConnection) { + console.log(`[ConnectionManager] Registering onStvConnection handler`); this.stvWebSocket.addEventListener("connection", handlers.onStvConnection); } } From a73392cc1180e7d694ea67349141b6fa52e34d1f Mon Sep 17 00:00:00 2001 From: BP602 Date: Mon, 22 Sep 2025 23:05:57 +0200 Subject: [PATCH 5/5] fix(7tv): eliminate redundant emote refresh after websocket updates - Remove unnecessary refresh7TVEmotes() call from handleEmoteSetUpdate - WebSocket events already provide updated emote data, API call was redundant - Fixes excessive "Refreshing 7TV emotes..." messages in chat - Add entitlement.delete event handling for proper cosmetic removal - Restore proper Zustand subscription in Message component for live cosmetic updates - Add deduplication for global cosmetic events (cosmetic.create, entitlement.*) - Fix new chatroom 7TV subscription via connectionManager.addChatroom - Add initial cosmetics loading when connecting to chatrooms - Improve diagnostic logging throughout cosmetics flow --- src/preload/index.js | 3 +- .../src/components/Messages/Message.jsx | 20 +- .../components/Messages/ModActionMessage.jsx | 4 +- src/renderer/src/providers/ChatProvider.jsx | 87 +++++--- .../src/providers/CosmeticsProvider.jsx | 98 ++++++++- utils/services/connectionManager.js | 16 +- utils/services/seventv/sharedStvWebSocket.js | 194 +++++++++--------- utils/services/seventv/stvAPI.js | 118 ++++++++++- utils/services/seventv/stvWebsocket.js | 3 +- 9 files changed, 397 insertions(+), 146 deletions(-) diff --git a/src/preload/index.js b/src/preload/index.js index a7e23b5..f1900b1 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -35,7 +35,7 @@ import { getUpdateTitle, getClearChatroom, } from "../../utils/services/kick/kickAPI"; -import { getUserStvProfile, getChannelEmotes } from "../../utils/services/seventv/stvAPI"; +import { getUserStvProfile, getChannelEmotes, getChannelCosmetics } from "../../utils/services/seventv/stvAPI"; import Store from "electron-store"; @@ -498,6 +498,7 @@ if (process.contextIsolated) { // 7TV API stv: { getChannelEmotes, + getChannelCosmetics, }, // Utility functions diff --git a/src/renderer/src/components/Messages/Message.jsx b/src/renderer/src/components/Messages/Message.jsx index b7d551a..ed20035 100644 --- a/src/renderer/src/components/Messages/Message.jsx +++ b/src/renderer/src/components/Messages/Message.jsx @@ -4,7 +4,7 @@ import ModActionMessage from "./ModActionMessage"; import RegularMessage from "./RegularMessage"; import EmoteUpdateMessage from "./EmoteUpdateMessage"; import clsx from "clsx"; -import { useShallow } from "zustand/shallow"; +import { useShallow } from "zustand/react/shallow"; import useCosmeticsStore from "../../providers/CosmeticsProvider"; import useChatStore from "../../providers/ChatProvider"; import ReplyMessage from "./ReplyMessage"; @@ -41,15 +41,17 @@ const Message = ({ const getDeleteMessage = useChatStore(useShallow((state) => state.getDeleteMessage)); const [rightClickedEmote, setRightClickedEmote] = useState(null); - let userStyle; + const subscribedUserStyle = useCosmeticsStore( + useShallow((state) => { + if (!message?.sender || type === "replyThread" || type === "dialog") { + return null; + } - if (message?.sender && type !== "replyThread") { - if (type === "dialog") { - userStyle = dialogUserStyle; - } else { - userStyle = useCosmeticsStore(useShallow((state) => state.getUserStyle(message?.sender?.username))); - } - } + return state.getUserStyle(message.sender.username); + }), + ); + + const userStyle = type === "dialog" ? dialogUserStyle : subscribedUserStyle; // CheckIcon if user can moderate const canModerate = useMemo( diff --git a/src/renderer/src/components/Messages/ModActionMessage.jsx b/src/renderer/src/components/Messages/ModActionMessage.jsx index fc41b25..6d7af53 100644 --- a/src/renderer/src/components/Messages/ModActionMessage.jsx +++ b/src/renderer/src/components/Messages/ModActionMessage.jsx @@ -1,11 +1,9 @@ import { useCallback } from "react"; import { convertMinutesToHumanReadable } from "../../utils/ChatUtils"; import useCosmeticsStore from "../../providers/CosmeticsProvider"; -import { useShallow } from "zustand/react/shallow"; const ModActionMessage = ({ message, chatroomId, allStvEmotes, subscriberBadges, chatroomName, userChatroomInfo }) => { const { modAction, modActionDetails } = message; - const getUserStyle = useCosmeticsStore(useShallow((state) => state.getUserStyle)); const actionTaker = modActionDetails?.banned_by?.username || modActionDetails?.unbanned_by?.username; const moderator = actionTaker !== "moderated" ? actionTaker : "Bot"; @@ -20,7 +18,7 @@ const ModActionMessage = ({ message, chatroomId, allStvEmotes, subscriberBadges, const user = await window.app.kick.getUserChatroomInfo(chatroomName, usernameDialog); if (!user?.data?.id) return; - const userStyle = getUserStyle(usernameDialog); + const userStyle = useCosmeticsStore.getState().getUserStyle(usernameDialog); const userDialogInfo = { id: user.data.id, diff --git a/src/renderer/src/providers/ChatProvider.jsx b/src/renderer/src/providers/ChatProvider.jsx index 79896ab..f7b86ef 100644 --- a/src/renderer/src/providers/ChatProvider.jsx +++ b/src/renderer/src/providers/ChatProvider.jsx @@ -1227,6 +1227,24 @@ const useChatStore = create((set, get) => ({ if (pusher.chat.OPEN) { const channel7TVEmotes = await window.app.stv.getChannelEmotes(chatroom.streamerData.user_id); + // Load initial cosmetics (badges and paints) for the channel + try { + const channelCosmetics = await window.app.stv.getChannelCosmetics(chatroom.streamerData.user_id); + console.log('[7TV Cosmetics] Initial cosmetics loaded:', { + badges: channelCosmetics?.badges?.length || 0, + paints: channelCosmetics?.paints?.length || 0 + }); + + // Add cosmetics to the store + const addCosmetics = useCosmeticsStore?.getState()?.addCosmetics; + if (addCosmetics && channelCosmetics && (channelCosmetics.badges?.length > 0 || channelCosmetics.paints?.length > 0)) { + addCosmetics(channelCosmetics); + console.log('[7TV Cosmetics] Added initial cosmetics to store'); + } + } catch (error) { + console.error('[7TV Cosmetics] Failed to load initial cosmetics:', error); + } + if (channel7TVEmotes) { const seenEmoteNames = new Set(); @@ -1512,26 +1530,20 @@ const useChatStore = create((set, get) => ({ onStvMessage: (event) => { try { const { chatroomId, type, body } = event.detail; - console.log(`[ChatProvider] Received 7TV event from shared WebSocket`, { - type, - chatroomId, - hasBody: !!body, - entitlementUser: body?.object?.user?.username, - badgeId: body?.object?.user?.style?.badge_id, - paintId: body?.object?.user?.style?.paint_id, - badgeCount: body?.badges?.length, - paintCount: body?.paints?.length - }); - if (chatroomId) { - console.log(`[ChatProvider] Routing event ${type} to specific chatroom: ${chatroomId}`); get().handleStvMessage(chatroomId, event.detail); } else { - console.log(`[ChatProvider] Broadcasting event ${type} to all chatrooms (${chatrooms.length} total)`); - // Broadcast to all chatrooms if no specific chatroom - chatrooms.forEach(chatroom => { - get().handleStvMessage(chatroom.id, event.detail); - }); + // Handle global cosmetic events once instead of broadcasting to all chatrooms + if (type === 'cosmetic.create' || type === 'entitlement.create' || type === 'entitlement.delete') { + console.log(`[ChatProvider] Processing global ${type} event for ${body?.object?.user?.username || 'unknown'}`); + // Handle once with a null chatroomId to indicate global event + get().handleStvMessage(null, event.detail); + } else { + // Broadcast to all chatrooms if no specific chatroom (for non-cosmetic events) + chatrooms.forEach(chatroom => { + get().handleStvMessage(chatroom.id, event.detail); + }); + } } } catch (error) { console.error("[ChatProvider] Error handling 7TV message:", error); @@ -1925,7 +1937,6 @@ const useChatStore = create((set, get) => ({ case "emote_set.update": get().handleEmoteSetUpdate(chatroomId, body); break; - case "cosmetic.create": case "cosmetic.create": { console.log( `[ChatProvider] Applying cosmetic catalog update for ${chatroomId ?? 'all chatrooms'}`, @@ -1946,7 +1957,6 @@ const useChatStore = create((set, get) => ({ } break; } - break; case "entitlement.create": { const username = body?.object?.user?.connections?.find((c) => c.platform === "KICK")?.username; const transformedUsername = username?.replaceAll("-", "_").toLowerCase(); @@ -1967,6 +1977,26 @@ const useChatStore = create((set, get) => ({ } break; } + case "entitlement.delete": { + const username = body?.object?.user?.connections?.find((c) => c.platform === "KICK")?.username; + const transformedUsername = username?.replaceAll("-", "_").toLowerCase(); + console.log( + `[ChatProvider] Processing entitlement deletion for ${transformedUsername || 'unknown user'}`, + { + refId: body?.object?.ref_id, + kind: body?.object?.kind, + chatroomId, + }, + ); + const removeUserStyle = useCosmeticsStore?.getState()?.removeUserStyle; + if (removeUserStyle) { + console.log(`[ChatProvider] Calling CosmeticsStore.removeUserStyle for ${transformedUsername}`); + removeUserStyle(transformedUsername, body); + } else { + console.error(`[ChatProvider] CosmeticsStore.removeUserStyle method not available!`); + } + break; + } default: break; } @@ -2203,10 +2233,18 @@ const useChatStore = create((set, get) => ({ // Connect to chatroom get().connectToChatroom(newChatroom); - // Connect to 7TV WebSocket - // DISABLED: Using shared connection system via connectionManager.initializeConnections - console.log(`[ChatProvider] Skipping individual 7TV connection for new chatroom ${newChatroom.id} - using shared connection system`); - // get().connectToStvWebSocket(newChatroom); + // Connect to 7TV WebSocket via connectionManager + if (connectionManager) { + console.log(`[ChatProvider] Adding new chatroom ${newChatroom.id} to connectionManager for 7TV subscriptions`); + try { + await connectionManager.addChatroom(newChatroom); + console.log(`[ChatProvider] Successfully added chatroom ${newChatroom.id} to connectionManager`); + } catch (error) { + console.error(`[ChatProvider] Error adding chatroom ${newChatroom.id} to connectionManager:`, error); + } + } else { + console.warn(`[ChatProvider] ConnectionManager not available for new chatroom ${newChatroom.id}`); + } // Save to local storage localStorage.setItem("chatrooms", JSON.stringify([...savedChatrooms, newChatroom])); @@ -3241,9 +3279,6 @@ const useChatStore = create((set, get) => ({ // Clear emote cache to ensure new emotes are loaded from updated store clearChatroomEmoteCache(chatroomId); - // Refresh emote data to get the updated emote set - get().refresh7TVEmotes(chatroomId); - try { const processingDuration = performance.now() - startTime; // Record emote update metrics via IPC diff --git a/src/renderer/src/providers/CosmeticsProvider.jsx b/src/renderer/src/providers/CosmeticsProvider.jsx index 705b5e9..4cdc3c2 100644 --- a/src/renderer/src/providers/CosmeticsProvider.jsx +++ b/src/renderer/src/providers/CosmeticsProvider.jsx @@ -45,8 +45,12 @@ const useCosmeticsStore = create((set, get) => ({ set((state) => { const currentStyle = state.userStyles[transformedUsername] || {}; - if (currentStyle.badgeId === body.object.user.style.badge_id && currentStyle.paintId === body.object.user.style.paint_id) - return state; + const newBadgeId = body.object.user.style.badge_id; + const newPaintId = body.object.user.style.paint_id; + + if (currentStyle.badgeId === newBadgeId && currentStyle.paintId === newPaintId) { + return state; // Skip duplicate style update + } return { userStyles: { @@ -66,6 +70,82 @@ const useCosmeticsStore = create((set, get) => ({ }); }, + removeUserStyle: async (username, body) => { + console.log(`[CosmeticsStore DIAGNOSTIC] removeUserStyle called`, { + username, + hasBody: !!body, + refId: body?.object?.ref_id, + kind: body?.object?.kind, + fullBody: body + }); + + const transformedUsername = username.toLowerCase(); + const refId = body?.object?.ref_id; + const kind = body?.object?.kind; + + if (!refId || !kind) { + console.log(`[CosmeticsStore DIAGNOSTIC] Missing ref_id or kind in entitlement.delete`, { + refId, + kind + }); + return; + } + + console.log( + `[CosmeticsStore DIAGNOSTIC] Removing ${kind} entitlement for ${transformedUsername}`, + { + refId, + kind + } + ); + + set((state) => { + const currentStyle = state.userStyles[transformedUsername]; + if (!currentStyle) { + console.log(`[CosmeticsStore DIAGNOSTIC] No existing style for ${transformedUsername}, nothing to remove`); + return state; + } + + // Based on Chatterino spec: remove by kind and ref_id + let updatedStyle = { ...currentStyle }; + let hasChanges = false; + + if (kind === "BADGE" && currentStyle.badgeId === refId) { + updatedStyle.badgeId = null; + hasChanges = true; + console.log(`[CosmeticsStore DIAGNOSTIC] Removed badge ${refId} from ${transformedUsername}`); + } else if (kind === "PAINT" && currentStyle.paintId === refId) { + updatedStyle.paintId = null; + hasChanges = true; + console.log(`[CosmeticsStore DIAGNOSTIC] Removed paint ${refId} from ${transformedUsername}`); + } + + if (!hasChanges) { + console.log(`[CosmeticsStore DIAGNOSTIC] No matching ${kind} with ref_id ${refId} found for ${transformedUsername}`); + return state; + } + + // If both badge and paint are removed, remove the entire user style entry + if (!updatedStyle.badgeId && !updatedStyle.paintId) { + const { [transformedUsername]: removed, ...restUserStyles } = state.userStyles; + console.log(`[CosmeticsStore DIAGNOSTIC] Removed entire user style for ${transformedUsername} (no remaining cosmetics)`); + return { + userStyles: restUserStyles, + }; + } + + return { + userStyles: { + ...state.userStyles, + [transformedUsername]: { + ...updatedStyle, + updatedAt: new Date().toISOString(), + }, + }, + }; + }); + }, + getUserStyle: (username) => { if (!username) { console.log(`[CosmeticsStore DIAGNOSTIC] getUserStyle called with empty username`); @@ -73,6 +153,17 @@ const useCosmeticsStore = create((set, get) => ({ } const transformedUsername = username.toLowerCase(); + + // Track call frequency for debugging + if (!window.__getUserStyleCalls) window.__getUserStyleCalls = new Map(); + const now = Date.now(); + const lastCall = window.__getUserStyleCalls.get(transformedUsername) || 0; + const timeSinceLastCall = now - lastCall; + window.__getUserStyleCalls.set(transformedUsername, now); + + if (timeSinceLastCall < 100) { // Less than 100ms since last call + console.warn(`[CosmeticsStore WARNING] getUserStyle called for ${transformedUsername} only ${timeSinceLastCall}ms ago - possible render loop!`); + } const userStyle = get().userStyles[transformedUsername]; const globalCosmetics = get().globalCosmetics; @@ -82,7 +173,8 @@ const useCosmeticsStore = create((set, get) => ({ userStylePaintId: userStyle?.paintId, totalBadges: globalCosmetics?.badges?.length, totalPaints: globalCosmetics?.paints?.length, - userStyleObject: userStyle + userStyleObject: userStyle, + callStack: new Error().stack?.split('\n').slice(1, 6).join('\n') // Show top 5 stack frames }); if (!userStyle?.badgeId && !userStyle?.paintId) { diff --git a/utils/services/connectionManager.js b/utils/services/connectionManager.js index 2441b83..3e68be4 100644 --- a/utils/services/connectionManager.js +++ b/utils/services/connectionManager.js @@ -309,12 +309,21 @@ class ConnectionManager { }); span.addEvent('7tv_websocket_add_start'); + console.log(`[ConnectionManager DIAGNOSTIC]: About to call stvWebSocket.addChatroom`, { + chatroomId: chatroom.id, + userIdForSubscription: chatroom.streamerData.user_id, + stvId: stvId, + stvEmoteSetId: stvEmoteSetId, + hasWebSocket: !!this.stvWebSocket + }); this.stvWebSocket.addChatroom( chatroom.id, - chatroom.streamerData.id, // Use the Kick channel ID for cosmetic/entitlement subscriptions + chatroom.streamerData.user_id, // Use the correct Kick user ID for cosmetic/entitlement subscriptions stvId, - stvEmoteSetId + stvEmoteSetId, + chatroom // Pass the full chatroom data for ID variants ); + console.log(`[ConnectionManager DIAGNOSTIC]: stvWebSocket.addChatroom completed for ${chatroom.id}`); span.addEvent('7tv_websocket_add_complete'); // Fetch initial messages for this chatroom @@ -556,6 +565,8 @@ class ConnectionManager { const channel7TVEmotes = await window.app.stv.getChannelEmotes(chatroom.streamerData.user_id); span.addEvent('api_fetch_complete'); + // Note: Cosmetics (badges/paints) are loaded dynamically via WebSocket events, not API calls + if (channel7TVEmotes) { this.channelStvEmoteCache.set(cacheKey, channel7TVEmotes); span.addEvent('cache_stored'); @@ -594,6 +605,7 @@ class ConnectionManager { chatroom.streamerData?.id, stvId, stvEmoteSetId, + chatroom // Pass the full chatroom data for ID variants ); } diff --git a/utils/services/seventv/sharedStvWebSocket.js b/utils/services/seventv/sharedStvWebSocket.js index 40831c2..75ab263 100644 --- a/utils/services/seventv/sharedStvWebSocket.js +++ b/utils/services/seventv/sharedStvWebSocket.js @@ -28,11 +28,13 @@ const updateCosmetics = async (body) => { return; } - cosmetics.badges.push({ + const newBadge = { id: data.id === "00000000000000000000000000" ? data.ref_id || "default_id" : data.id, title: data.tooltip, url: `https:${data.host.url}/${data.host.files[data.host.files.length - 1].name}`, - }); + }; + cosmetics.badges.push(newBadge); + console.log(`[Shared7TV] Added badge: ${newBadge.title}`); } } else if (object?.kind === "PAINT") { if (!object.user) { @@ -117,6 +119,7 @@ const updateCosmetics = async (body) => { } cosmetics.paints.push(push); + console.log(`[Shared7TV] Added paint: ${push.name}`); } } else if ( object?.name === "Personal Emotes" || @@ -167,20 +170,51 @@ class SharedStvWebSocket extends EventTarget { this.connectionSpan = null; // Track current connection span } - addChatroom(chatroomId, channelKickID, stvId = "0", stvEmoteSetId = "0") { - this.chatrooms.set(chatroomId, { + addChatroom(chatroomId, channelKickID, stvId = "0", stvEmoteSetId = "0", chatroomData = null) { + // Store all available ID variants for diagnostic subscriptions + const storedData = { channelKickID: String(channelKickID), stvId, stvEmoteSetId, - }); + }; + + // Add additional ID variants if chatroomData is provided + if (chatroomData) { + storedData.idVariants = { + chatroom_id: String(chatroomId), + streamer_id: String(channelKickID), // This is chatroomData.streamerData.id + streamer_user_id: chatroomData.streamerData?.user_id ? String(chatroomData.streamerData.user_id) : null, + username: chatroomData.streamerData?.username || null, + user_username: chatroomData.streamerData?.user?.username || null, + slug: chatroomData.slug || null, + }; + } else { + // Fallback for legacy calls - just store the basic IDs + storedData.idVariants = { + chatroom_id: String(chatroomId), + streamer_id: String(channelKickID), + }; + } + + this.chatrooms.set(chatroomId, storedData); console.log( `[Shared7TV]: Registered chatroom ${chatroomId} (kick=${channelKickID}, stvUser=${stvId}, stvSet=${stvEmoteSetId})`, ); // If we're already connected, subscribe to this chatroom's events + console.log(`[Shared7TV DIAGNOSTIC]: addChatroom called for ${chatroomId}`, { + connectionState: this.connectionState, + willSubscribe: this.connectionState === 'connected', + totalChatrooms: this.chatrooms.size, + channelKickID: channelKickID + }); + if (this.connectionState === 'connected') { + console.log(`[Shared7TV DIAGNOSTIC]: Subscribing to events for new chatroom ${chatroomId}`); this.subscribeToChatroomEvents(chatroomId); + } else { + console.log(`[Shared7TV DIAGNOSTIC]: WebSocket not connected (${this.connectionState}), will subscribe when connected`); } } @@ -429,6 +463,8 @@ class SharedStvWebSocket extends EventTarget { } async subscribeToChatroomEvents(chatroomId) { + console.log(`[Shared7TV DIAGNOSTIC]: subscribeToChatroomEvents called for ${chatroomId}`); + const chatroomData = this.chatrooms.get(chatroomId); if (!chatroomData) { console.log(`[Shared7TV]: Chatroom ${chatroomId} not found`); @@ -491,7 +527,6 @@ class SharedStvWebSocket extends EventTarget { const subscribeUserMessage = { op: 35, - t: Date.now(), d: { type: "user.*", condition: { object_id: chatroomWithStvId.stvId }, @@ -505,11 +540,9 @@ class SharedStvWebSocket extends EventTarget { } /** - * Subscribe to cosmetic events for a specific chatroom + * Subscribe to cosmetic events for a specific chatroom (using wildcard like Firefox extension) */ async subscribeToCosmeticEvents(chatroomId, channelKickID) { - console.log(`[Shared7TV]: Attempting to subscribe to cosmetic events for chatroom ${chatroomId}, channelKickID: ${channelKickID}`); - if (!this.chat || this.chat.readyState !== WebSocket.OPEN) { console.log(`[Shared7TV]: Cannot subscribe to cosmetic events - WebSocket not ready. State: ${this.chat?.readyState}`); return; @@ -517,33 +550,27 @@ class SharedStvWebSocket extends EventTarget { const eventKey = `cosmetic.*:${channelKickID}`; if (this.subscribedEvents.has(eventKey)) { - console.log( - `[Shared7TV]: Cosmetic subscription already active for Kick channel ${channelKickID} (chatroom ${chatroomId})`, - ); + console.log(`[Shared7TV]: Cosmetic subscription already exists for channel ${channelKickID}`); return; } - const subscribeAllCosmetics = { + const subscribeCosmeticAll = { op: 35, - t: Date.now(), d: { type: "cosmetic.*", - condition: { platform: "KICK", ctx: "channel", id: channelKickID }, + condition: { ctx: "channel", platform: "KICK", id: channelKickID }, }, }; - console.log(`[Shared7TV]: Sending cosmetic subscription message:`, subscribeAllCosmetics); - this.chat.send(JSON.stringify(subscribeAllCosmetics)); + console.log(`[Shared7TV]: Subscribing to cosmetic.* events for channel ${channelKickID}`); + this.chat.send(JSON.stringify(subscribeCosmeticAll)); this.subscribedEvents.add(eventKey); - console.log(`[Shared7TV]: Subscribed to cosmetic.* events for chatroom ${chatroomId}`); } /** - * Subscribe to entitlement events for a specific chatroom + * Subscribe to entitlement events for a specific chatroom (using wildcard like Firefox extension) */ async subscribeToEntitlementEvents(chatroomId, channelKickID) { - console.log(`[Shared7TV]: Attempting to subscribe to entitlement events for chatroom ${chatroomId}, channelKickID: ${channelKickID}`); - if (!this.chat || this.chat.readyState !== WebSocket.OPEN) { console.log(`[Shared7TV]: Cannot subscribe to entitlement events - WebSocket not ready. State: ${this.chat?.readyState}`); return; @@ -551,35 +578,23 @@ class SharedStvWebSocket extends EventTarget { const eventKey = `entitlement.*:${channelKickID}`; if (this.subscribedEvents.has(eventKey)) { - console.log( - `[Shared7TV]: Entitlement subscription already active for Kick channel ${channelKickID} (chatroom ${chatroomId})`, - ); + console.log(`[Shared7TV]: Entitlement subscription already exists for channel ${channelKickID}`); return; } - const subscribeAllEntitlements = { + const subscribeEntitlementAll = { op: 35, - t: Date.now(), d: { type: "entitlement.*", - condition: { platform: "KICK", ctx: "channel", id: channelKickID }, + condition: { ctx: "channel", platform: "KICK", id: channelKickID }, }, }; - console.log(`[Shared7TV]: Sending entitlement subscription message:`, subscribeAllEntitlements); - this.chat.send(JSON.stringify(subscribeAllEntitlements)); + console.log(`[Shared7TV]: Subscribing to entitlement.* events for channel ${channelKickID}`); + this.chat.send(JSON.stringify(subscribeEntitlementAll)); this.subscribedEvents.add(eventKey); - console.log(`[Shared7TV]: Subscribed to entitlement.* events for chatroom ${chatroomId}`); - - this.dispatchEvent( - new CustomEvent("open", { - detail: { - body: "SUBSCRIBED", - type: "entitlement.*", - chatroomId, - }, - }), - ); + + this.dispatchEvent(new CustomEvent("open", { detail: { body: "SUBSCRIBED", type: "entitlement.*" } })); } /** @@ -604,7 +619,6 @@ class SharedStvWebSocket extends EventTarget { const subscribeAllEmoteSets = { op: 35, - t: Date.now(), d: { type: "emote_set.*", condition: { object_id: stvEmoteSetId }, @@ -618,24 +632,24 @@ class SharedStvWebSocket extends EventTarget { setupMessageHandler() { this.chat.onmessage = (event) => { - console.log(`[Shared7TV]: Raw WebSocket message received:`, event.data); try { const msg = JSON.parse(event.data); - console.log(`[Shared7TV]: Parsed message:`, { - op: msg?.op, - type: msg?.d?.type, - hasBody: !!msg?.d?.body, - fullMessage: msg - }); + // Only log non-heartbeat messages + if (msg?.op !== 2) { + console.log(`[Shared7TV]: Message received:`, { + op: msg?.op, + type: msg?.d?.type, + hasBody: !!msg?.d?.body + }); + } // Handle different 7TV opcodes switch (msg?.op) { case 0: // Dispatch (actual events) - console.log(`[Shared7TV]: Dispatch event received`, { type: msg?.d?.type }); try { window.app?.telemetry?.recordWebSocketMessage?.('7tv_shared', 'dispatch', msg?.d?.type); } catch (_) {} - break; + break; // Break here to continue to event processing logic below the switch statement case 1: // Hello (connection established) console.log(`[Shared7TV]: Hello received`, { heartbeat_interval: msg?.d?.heartbeat_interval, @@ -750,19 +764,9 @@ class SharedStvWebSocket extends EventTarget { const { body, type } = msg.d; - // DIAGNOSTIC: Log all cosmetic-related events with detailed info + // Log cosmetic-related events (condensed) if (type?.includes('cosmetic') || type?.includes('entitlement')) { - console.log(`[Shared7TV DIAGNOSTIC]: Cosmetic event received`, { - type: type, - objectKind: body?.object?.kind, - objectId: body?.object?.id, - userId: body?.object?.user?.id, - username: body?.object?.user?.username, - platform: body?.context?.platform || body?.condition?.platform, - channelId: body?.context?.id || body?.condition?.id, - hasData: !!body?.object?.data, - fullBody: body - }); + console.log(`[Shared7TV]: ${type} event for ${body?.object?.user?.username || 'unknown'}`); } // Find which chatroom this event belongs to @@ -806,19 +810,6 @@ class SharedStvWebSocket extends EventTarget { case "cosmetic.create": updateCosmetics(body); - console.log( - `[Shared7TV]: Forwarding cosmetic catalog update to ${chatroomId === null ? 'all chatrooms' : chatroomId}`, - { - badgeCount: cosmetics?.badges?.length, - paintCount: cosmetics?.paints?.length, - }, - ); - - console.log(`[Shared7TV]: Dispatching cosmetic.create event for chatroomId: ${chatroomId}`, { - badgeCount: cosmetics?.badges?.length, - paintCount: cosmetics?.paints?.length, - eventDetail: { chatroomId, type: "cosmetic.create" } - }); this.dispatchEvent( new CustomEvent("message", { detail: { @@ -831,21 +822,8 @@ class SharedStvWebSocket extends EventTarget { break; case "entitlement.create": - if (body.kind === 10) { - console.log( - `[Shared7TV]: Forwarding entitlement for ${body?.object?.user?.username || 'unknown user'}`, - { - chatroomId, - badgeId: body?.object?.user?.style?.badge_id, - paintId: body?.object?.user?.style?.paint_id, - }, - ); - console.log(`[Shared7TV]: Dispatching entitlement.create event for chatroomId: ${chatroomId}`, { - username: body?.object?.user?.username, - badgeId: body?.object?.user?.style?.badge_id, - paintId: body?.object?.user?.style?.paint_id, - eventDetail: { chatroomId, type: "entitlement.create" } - }); + // Process entitlements for both cosmetics (kind 10) and emote sets (kind 5) + if (body.kind === 10 || body.kind === 5) { this.dispatchEvent( new CustomEvent("message", { detail: { @@ -857,6 +835,18 @@ class SharedStvWebSocket extends EventTarget { ); } break; + + case "entitlement.delete": + this.dispatchEvent( + new CustomEvent("message", { + detail: { + body, + type: "entitlement.delete", + chatroomId, + }, + }), + ); + break; } } catch (error) { console.log("[Shared7TV] Error parsing message:", error); @@ -866,7 +856,6 @@ class SharedStvWebSocket extends EventTarget { findChatroomForEvent(body, type) { // Try to identify which chatroom this event belongs to - // This is a best-effort approach since 7TV events don't always include channel context // For user events, broadcast to all chatrooms if (type.startsWith("user.")) { @@ -882,17 +871,22 @@ class SharedStvWebSocket extends EventTarget { } } - // For cosmetic and entitlement events, they should include channel context - // but if not, we'll broadcast to all chatrooms + // For cosmetic and entitlement events, check all ID variants to see which subscription received this if (type.startsWith("cosmetic.") || type.startsWith("entitlement.")) { const contextId = body?.context?.id || body?.condition?.id || null; - console.log( - `[Shared7TV]: Unable to directly map ${type} to a chatroom, broadcasting`, - { - contextId, - knownChatrooms: Array.from(this.chatrooms.values()).map((data) => data.channelKickID), - }, - ); + + if (contextId) { + // Check each chatroom's ID variants to see which one matches + for (const [chatroomId, data] of this.chatrooms) { + const idVariants = data.idVariants || {}; + + for (const [idType, idValue] of Object.entries(idVariants)) { + if (idValue && String(idValue) === String(contextId)) { + return chatroomId; + } + } + } + } } return null; diff --git a/utils/services/seventv/stvAPI.js b/utils/services/seventv/stvAPI.js index 6169381..3a02cf8 100644 --- a/utils/services/seventv/stvAPI.js +++ b/utils/services/seventv/stvAPI.js @@ -251,4 +251,120 @@ const getUserStvProfile = async (platformId) => { } }; -export { getChannelEmotes, sendUserPresence, getUserStvProfile }; +const getChannelCosmetics = async (channelId) => { + try { + console.log("[7TV Cosmetics] Fetching global cosmetics catalog"); + + // Fetch the global cosmetics catalog using v2 API (v3 removed cosmetics endpoints) + // This provides the base catalog of badges and paints that WebSocket events reference + const response = await axios.get(`https://api.7tv.app/v2/cosmetics`); + + if (response.status !== 200 || !response.data) { + console.warn("[7TV Cosmetics] Failed to fetch cosmetics:", response.status); + return { badges: [], paints: [] }; + } + + const cosmeticsData = response.data; + const cosmetics = { badges: [], paints: [] }; + + // Process badges from v2 API response + if (cosmeticsData.badges?.length) { + cosmetics.badges = cosmeticsData.badges.map(badge => ({ + id: badge.id, + title: badge.tooltip || badge.name, + url: `https:${badge.urls?.[badge.urls.length - 1] || badge.urls?.[0]}`, + })); + console.log(`[7TV Cosmetics] Loaded ${cosmetics.badges.length} badges`); + } + + // Process paints from v2 API response + if (cosmeticsData.paints?.length) { + cosmetics.paints = cosmeticsData.paints.map(paint => { + const randomColor = "#00f742"; + let paintObject = {}; + + if (paint.data?.stops?.length) { + const normalizedColors = paint.data.stops.map((stop) => ({ + at: stop.at * 100, + color: stop.color, + })); + + const gradient = normalizedColors.map((stop) => `${argbToRgba(stop.color)} ${stop.at}%`).join(", "); + + let paintFunction = paint.data.function?.toLowerCase().replace("_", "-"); + if (paint.data.repeat) { + paintFunction = `repeating-${paintFunction}`; + } + + let isDeg_or_Shape = `${paint.data.angle}deg`; + if (paintFunction !== "linear-gradient" && paintFunction !== "repeating-linear-gradient") { + isDeg_or_Shape = paint.data.shape; + } + + paintObject = { + id: paint.id, + name: paint.data.name, + style: paint.data.function, + shape: paint.data.shape, + backgroundImage: `${paintFunction || "linear-gradient"}(${isDeg_or_Shape}, ${gradient})` || + `${paint.data.style || "linear-gradient"}(${paint.data.shape || ""} 0deg, ${randomColor}, ${randomColor})`, + shadows: null, + KIND: "non-animated", + url: paint.data.image_url, + }; + } else { + paintObject = { + id: paint.id, + name: paint.data.name, + style: paint.data.function, + shape: paint.data.shape, + backgroundImage: `url('${paint.data.image_url}')` || + `${paint.data.style || "linear-gradient"}(${paint.data.shape || ""} 0deg, ${randomColor}, ${randomColor})`, + shadows: null, + KIND: "animated", + url: paint.data.image_url, + }; + } + + // Process shadows if present + if (paint.data.shadows?.length) { + const shadow = paint.data.shadows + .map((shadow) => { + let rgbaColor = argbToRgba(shadow.color); + rgbaColor = rgbaColor.replace(/rgba\((\d+), (\d+), (\d+), (\d+(\.\d+)?)\)/, `rgba($1, $2, $3)`); + return `drop-shadow(${rgbaColor} ${shadow.x_offset}px ${shadow.y_offset}px ${shadow.radius}px)`; + }) + .join(" "); + paintObject.shadows = shadow; + } + + return paintObject; + }); + console.log(`[7TV Cosmetics] Loaded ${cosmetics.paints.length} paints`); + } + + console.log("[7TV Cosmetics] Total cosmetics loaded:", { + badges: cosmetics.badges.length, + paints: cosmetics.paints.length + }); + + return cosmetics; + } catch (error) { + console.error("[7TV Cosmetics] Error fetching cosmetics:", error.message); + return { badges: [], paints: [] }; + } +}; + +// Helper function to convert ARGB to RGBA +const argbToRgba = (color) => { + if (color < 0) { + color = color >>> 0; + } + + const red = (color >> 24) & 0xff; + const green = (color >> 16) & 0xff; + const blue = (color >> 8) & 0xff; + return `rgba(${red}, ${green}, ${blue}, 1)`; +}; + +export { getChannelEmotes, sendUserPresence, getUserStvProfile, getChannelCosmetics }; diff --git a/utils/services/seventv/stvWebsocket.js b/utils/services/seventv/stvWebsocket.js index 20d71c8..a0aa455 100644 --- a/utils/services/seventv/stvWebsocket.js +++ b/utils/services/seventv/stvWebsocket.js @@ -501,7 +501,8 @@ class StvWebSocket extends EventTarget { break; case "entitlement.create": - if (body.kind === 10) { + // Process entitlements for both cosmetics (kind 10) and emote sets (kind 5) + if (body.kind === 10 || body.kind === 5) { this.dispatchEvent( new CustomEvent("message", { detail: { body, type: "entitlement.create" },