From 649796c9736938c2b5b110a7224cb1fcb76cf306 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 28 Jul 2026 09:53:03 +0100 Subject: [PATCH 1/3] fix(webapp): stop logging full batch item contents in batchTriggerV3 The idempotency grouping step logged every batch item verbatim, including its payload and options (which can carry arbitrary customer metadata). Replace that with a summary of task identifiers and item counts, and add a unit test covering the summary shape. --- .../app/v3/services/batchTriggerV3.server.ts | 28 ++++++++++++-- .../test/batchTriggerV3ItemLogSummary.test.ts | 38 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 apps/webapp/test/batchTriggerV3ItemLogSummary.test.ts diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index fdb87d2d2c..e3e8be7af8 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -43,6 +43,27 @@ function chunkArray(items: T[], size: number): T[][] { } return chunks; } + +// Summarizes a task-identifier -> items grouping for logging, without including the items +// themselves (each item is `{ task, payload, options }`, and `options.metadata` is arbitrary +// customer data that must never be written to logs whole). +export function summarizeItemsByTask( + itemsByTask: Record +): { taskIdentifiers: string[]; itemCountsByTask: Record; totalItemCount: number } { + const itemCountsByTask: Record = {}; + let totalItemCount = 0; + + for (const [taskIdentifier, items] of Object.entries(itemsByTask)) { + itemCountsByTask[taskIdentifier] = items.length; + totalItemCount += items.length; + } + + return { + taskIdentifiers: Object.keys(itemsByTask), + itemCountsByTask, + totalItemCount, + }; +} const ASYNC_BATCH_PROCESS_SIZE_THRESHOLD = 20; const MAX_ATTEMPTS = 10; @@ -409,9 +430,10 @@ export class BatchTriggerV3Service extends BaseService { {} as Record ); - logger.debug("[BatchTriggerV2][call] Grouped items by task identifier", { - itemsByTask, - }); + logger.debug( + "[BatchTriggerV2][call] Grouped items by task identifier", + summarizeItemsByTask(itemsByTask) + ); const idempotencyKeyLookups = Object.entries(itemsByTask).flatMap(([taskIdentifier, items]) => { const idempotencyKeys = Array.from( diff --git a/apps/webapp/test/batchTriggerV3ItemLogSummary.test.ts b/apps/webapp/test/batchTriggerV3ItemLogSummary.test.ts new file mode 100644 index 0000000000..fd0e603d91 --- /dev/null +++ b/apps/webapp/test/batchTriggerV3ItemLogSummary.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "vitest"; +import { summarizeItemsByTask } from "~/v3/services/batchTriggerV3.server"; + +describe("summarizeItemsByTask", () => { + test("returns counts and task identifiers without the underlying items", () => { + const itemsByTask = { + "my-task": [ + { task: "my-task", payload: { secret: "value" }, options: { metadata: { pii: "yes" } } }, + { task: "my-task", payload: { secret: "value-2" }, options: {} }, + ], + "other-task": [{ task: "other-task", payload: {}, options: {} }], + }; + + const summary = summarizeItemsByTask(itemsByTask); + + expect(summary).toEqual({ + taskIdentifiers: ["my-task", "other-task"], + itemCountsByTask: { "my-task": 2, "other-task": 1 }, + totalItemCount: 3, + }); + + // The summary must never contain the raw items, their payloads, or their options - + // just identifiers and counts. + const serialized = JSON.stringify(summary); + expect(serialized).not.toContain("payload"); + expect(serialized).not.toContain("secret"); + expect(serialized).not.toContain("metadata"); + expect(serialized).not.toContain("pii"); + }); + + test("returns empty collections when there are no idempotent items", () => { + expect(summarizeItemsByTask({})).toEqual({ + taskIdentifiers: [], + itemCountsByTask: {}, + totalItemCount: 0, + }); + }); +}); From 1e89a99babde540f70e363d21165f98aac31c5a6 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 28 Jul 2026 10:37:03 +0100 Subject: [PATCH 2/3] chore(webapp): format batch trigger item logging --- apps/webapp/app/v3/services/batchTriggerV3.server.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index e3e8be7af8..2b19afcfb1 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -47,9 +47,11 @@ function chunkArray(items: T[], size: number): T[][] { // Summarizes a task-identifier -> items grouping for logging, without including the items // themselves (each item is `{ task, payload, options }`, and `options.metadata` is arbitrary // customer data that must never be written to logs whole). -export function summarizeItemsByTask( - itemsByTask: Record -): { taskIdentifiers: string[]; itemCountsByTask: Record; totalItemCount: number } { +export function summarizeItemsByTask(itemsByTask: Record): { + taskIdentifiers: string[]; + itemCountsByTask: Record; + totalItemCount: number; +} { const itemCountsByTask: Record = {}; let totalItemCount = 0; From 89d0eaa67aed41db6d4ccd83ed941e9c98686f87 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 29 Jul 2026 17:37:16 +0100 Subject: [PATCH 3/3] remove log altogether --- .../app/v3/services/batchTriggerV3.server.ts | 28 -------------- .../test/batchTriggerV3ItemLogSummary.test.ts | 38 ------------------- 2 files changed, 66 deletions(-) delete mode 100644 apps/webapp/test/batchTriggerV3ItemLogSummary.test.ts diff --git a/apps/webapp/app/v3/services/batchTriggerV3.server.ts b/apps/webapp/app/v3/services/batchTriggerV3.server.ts index 2b19afcfb1..563ef446bc 100644 --- a/apps/webapp/app/v3/services/batchTriggerV3.server.ts +++ b/apps/webapp/app/v3/services/batchTriggerV3.server.ts @@ -43,29 +43,6 @@ function chunkArray(items: T[], size: number): T[][] { } return chunks; } - -// Summarizes a task-identifier -> items grouping for logging, without including the items -// themselves (each item is `{ task, payload, options }`, and `options.metadata` is arbitrary -// customer data that must never be written to logs whole). -export function summarizeItemsByTask(itemsByTask: Record): { - taskIdentifiers: string[]; - itemCountsByTask: Record; - totalItemCount: number; -} { - const itemCountsByTask: Record = {}; - let totalItemCount = 0; - - for (const [taskIdentifier, items] of Object.entries(itemsByTask)) { - itemCountsByTask[taskIdentifier] = items.length; - totalItemCount += items.length; - } - - return { - taskIdentifiers: Object.keys(itemsByTask), - itemCountsByTask, - totalItemCount, - }; -} const ASYNC_BATCH_PROCESS_SIZE_THRESHOLD = 20; const MAX_ATTEMPTS = 10; @@ -432,11 +409,6 @@ export class BatchTriggerV3Service extends BaseService { {} as Record ); - logger.debug( - "[BatchTriggerV2][call] Grouped items by task identifier", - summarizeItemsByTask(itemsByTask) - ); - const idempotencyKeyLookups = Object.entries(itemsByTask).flatMap(([taskIdentifier, items]) => { const idempotencyKeys = Array.from( new Set( diff --git a/apps/webapp/test/batchTriggerV3ItemLogSummary.test.ts b/apps/webapp/test/batchTriggerV3ItemLogSummary.test.ts deleted file mode 100644 index fd0e603d91..0000000000 --- a/apps/webapp/test/batchTriggerV3ItemLogSummary.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { summarizeItemsByTask } from "~/v3/services/batchTriggerV3.server"; - -describe("summarizeItemsByTask", () => { - test("returns counts and task identifiers without the underlying items", () => { - const itemsByTask = { - "my-task": [ - { task: "my-task", payload: { secret: "value" }, options: { metadata: { pii: "yes" } } }, - { task: "my-task", payload: { secret: "value-2" }, options: {} }, - ], - "other-task": [{ task: "other-task", payload: {}, options: {} }], - }; - - const summary = summarizeItemsByTask(itemsByTask); - - expect(summary).toEqual({ - taskIdentifiers: ["my-task", "other-task"], - itemCountsByTask: { "my-task": 2, "other-task": 1 }, - totalItemCount: 3, - }); - - // The summary must never contain the raw items, their payloads, or their options - - // just identifiers and counts. - const serialized = JSON.stringify(summary); - expect(serialized).not.toContain("payload"); - expect(serialized).not.toContain("secret"); - expect(serialized).not.toContain("metadata"); - expect(serialized).not.toContain("pii"); - }); - - test("returns empty collections when there are no idempotent items", () => { - expect(summarizeItemsByTask({})).toEqual({ - taskIdentifiers: [], - itemCountsByTask: {}, - totalItemCount: 0, - }); - }); -});