From 2a5587c128b283fd05781c7cec3672c5e49b9bdd Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 11 Jan 2026 07:20:01 -0800 Subject: [PATCH] Show sent/received cookie counts in Cookies tab - Add getCookieCounts function to parse cookie headers and count individual cookies (not just headers) - Deduplicates by cookie name using Sets - Display as sent/received format like Headers tab - Add showZero to CountBadge so 0/3 displays properly - Add tests for getCookieCounts --- src-web/components/HttpResponsePane.tsx | 31 ++++----- src-web/lib/model_util.test.ts | 93 +++++++++++++++++++++++++ src-web/lib/model_util.ts | 35 +++++++++- 3 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 src-web/lib/model_util.test.ts diff --git a/src-web/components/HttpResponsePane.tsx b/src-web/components/HttpResponsePane.tsx index 82d1982b..a83cf73d 100644 --- a/src-web/components/HttpResponsePane.tsx +++ b/src-web/components/HttpResponsePane.tsx @@ -9,7 +9,7 @@ import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { useResponseBodyBytes, useResponseBodyText } from '../hooks/useResponseBodyText'; import { useResponseViewMode } from '../hooks/useResponseViewMode'; import { getMimeTypeFromContentType } from '../lib/contentType'; -import { getContentTypeFromHeaders } from '../lib/model_util'; +import { getCookieCounts, getContentTypeFromHeaders } from '../lib/model_util'; import { ConfirmLargeResponse } from './ConfirmLargeResponse'; import { ConfirmLargeResponseRequest } from './ConfirmLargeResponseRequest'; import { Banner } from './core/Banner'; @@ -67,20 +67,10 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) { const responseEvents = useHttpResponseEvents(activeResponse); - const cookieCount = useMemo(() => { - if (!responseEvents.data) return 0; - let count = 0; - for (const event of responseEvents.data) { - const e = event.event; - if ( - (e.type === 'header_up' && e.name.toLowerCase() === 'cookie') || - (e.type === 'header_down' && e.name.toLowerCase() === 'set-cookie') - ) { - count++; - } - } - return count; - }, [responseEvents.data]); + const cookieCounts = useMemo( + () => getCookieCounts(responseEvents.data), + [responseEvents.data], + ); const tabs = useMemo( () => [ @@ -107,15 +97,19 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) { label: 'Headers', rightSlot: ( ), }, { value: TAB_COOKIES, label: 'Cookies', - rightSlot: cookieCount > 0 ? : null, + rightSlot: + cookieCounts.sent > 0 || cookieCounts.received > 0 ? ( + + ) : null, }, { value: TAB_TIMELINE, @@ -127,7 +121,8 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) { activeResponse?.headers, activeResponse?.requestContentLength, activeResponse?.requestHeaders.length, - cookieCount, + cookieCounts.sent, + cookieCounts.received, mimeType, responseEvents.data?.length, setViewMode, diff --git a/src-web/lib/model_util.test.ts b/src-web/lib/model_util.test.ts new file mode 100644 index 00000000..4cf179ab --- /dev/null +++ b/src-web/lib/model_util.test.ts @@ -0,0 +1,93 @@ +import type { HttpResponseEvent } from '@yaakapp-internal/models'; +import { describe, expect, test } from 'vitest'; +import { getCookieCounts } from './model_util'; + +function makeEvent( + type: string, + name: string, + value: string, +): HttpResponseEvent { + return { + id: 'test', + model: 'http_response_event', + responseId: 'resp', + createdAt: Date.now(), + event: { type, name, value } as HttpResponseEvent['event'], + }; +} + +describe('getCookieCounts', () => { + test('returns zeros for undefined events', () => { + expect(getCookieCounts(undefined)).toEqual({ sent: 0, received: 0 }); + }); + + test('returns zeros for empty events', () => { + expect(getCookieCounts([])).toEqual({ sent: 0, received: 0 }); + }); + + test('counts single sent cookie', () => { + const events = [makeEvent('header_up', 'Cookie', 'session=abc123')]; + expect(getCookieCounts(events)).toEqual({ sent: 1, received: 0 }); + }); + + test('counts multiple sent cookies in one header', () => { + const events = [makeEvent('header_up', 'Cookie', 'a=1; b=2; c=3')]; + expect(getCookieCounts(events)).toEqual({ sent: 3, received: 0 }); + }); + + test('counts single received cookie', () => { + const events = [makeEvent('header_down', 'Set-Cookie', 'session=abc123; Path=/')]; + expect(getCookieCounts(events)).toEqual({ sent: 0, received: 1 }); + }); + + test('counts multiple received cookies from multiple headers', () => { + const events = [ + makeEvent('header_down', 'Set-Cookie', 'a=1; Path=/'), + makeEvent('header_down', 'Set-Cookie', 'b=2; HttpOnly'), + makeEvent('header_down', 'Set-Cookie', 'c=3; Secure'), + ]; + expect(getCookieCounts(events)).toEqual({ sent: 0, received: 3 }); + }); + + test('deduplicates sent cookies by name', () => { + const events = [ + makeEvent('header_up', 'Cookie', 'session=old'), + makeEvent('header_up', 'Cookie', 'session=new'), + ]; + expect(getCookieCounts(events)).toEqual({ sent: 1, received: 0 }); + }); + + test('deduplicates received cookies by name', () => { + const events = [ + makeEvent('header_down', 'Set-Cookie', 'token=abc; Path=/'), + makeEvent('header_down', 'Set-Cookie', 'token=xyz; Path=/'), + ]; + expect(getCookieCounts(events)).toEqual({ sent: 0, received: 1 }); + }); + + test('counts both sent and received cookies', () => { + const events = [ + makeEvent('header_up', 'Cookie', 'a=1; b=2; c=3'), + makeEvent('header_down', 'Set-Cookie', 'x=10; Path=/'), + makeEvent('header_down', 'Set-Cookie', 'y=20; Path=/'), + makeEvent('header_down', 'Set-Cookie', 'z=30; Path=/'), + ]; + expect(getCookieCounts(events)).toEqual({ sent: 3, received: 3 }); + }); + + test('ignores non-cookie headers', () => { + const events = [ + makeEvent('header_up', 'Content-Type', 'application/json'), + makeEvent('header_down', 'Content-Length', '123'), + ]; + expect(getCookieCounts(events)).toEqual({ sent: 0, received: 0 }); + }); + + test('handles case-insensitive header names', () => { + const events = [ + makeEvent('header_up', 'COOKIE', 'a=1'), + makeEvent('header_down', 'SET-COOKIE', 'b=2; Path=/'), + ]; + expect(getCookieCounts(events)).toEqual({ sent: 1, received: 1 }); + }); +}); diff --git a/src-web/lib/model_util.ts b/src-web/lib/model_util.ts index 7d1c9a92..76815e2d 100644 --- a/src-web/lib/model_util.ts +++ b/src-web/lib/model_util.ts @@ -1,4 +1,10 @@ -import type { AnyModel, Cookie, Environment, HttpResponseHeader } from '@yaakapp-internal/models'; +import type { + AnyModel, + Cookie, + Environment, + HttpResponseEvent, + HttpResponseHeader, +} from '@yaakapp-internal/models'; import { getMimeTypeFromContentType } from './contentType'; export const BODY_TYPE_NONE = null; @@ -59,3 +65,30 @@ export function isSubEnvironment(environment: Environment): boolean { export function isFolderEnvironment(environment: Environment): boolean { return environment.parentModel === 'folder'; } + +export function getCookieCounts( + events: HttpResponseEvent[] | undefined, +): { sent: number; received: number } { + if (!events) return { sent: 0, received: 0 }; + + // Use Sets to deduplicate by cookie name + const sentNames = new Set(); + const receivedNames = new Set(); + + for (const event of events) { + const e = event.event; + if (e.type === 'header_up' && e.name.toLowerCase() === 'cookie') { + // Parse "Cookie: name=value; name2=value2" format + for (const pair of e.value.split(';')) { + const name = pair.split('=')[0]?.trim(); + if (name) sentNames.add(name); + } + } else if (e.type === 'header_down' && e.name.toLowerCase() === 'set-cookie') { + // Parse "Set-Cookie: name=value; ..." - first part before ; is name=value + const name = e.value.split(';')[0]?.split('=')[0]?.trim(); + if (name) receivedNames.add(name); + } + } + + return { sent: sentNames.size, received: receivedNames.size }; +}