From 24e875d86fffe2ba8b06014073602c17d662ba0a Mon Sep 17 00:00:00 2001 From: Vojta Bartos Date: Mon, 29 Jun 2026 15:51:12 +0200 Subject: [PATCH 1/3] feat(llm-gateway): tag helper calls with $ai_span_name for analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helper LLM calls (commit messages, PR descriptions) currently surface as `$ai_generation` events that are indistinguishable from the agent's main generations — they share `ai_product = posthog_code` with no further metadata, so observability queries can't filter to a specific helper. - Add a free-form `posthogProperties` option to `LlmGatewayService.prompt()` that forwards each entry as an `x-posthog-property-` header. The gateway lifts those headers onto the captured `$ai_generation` event, mirroring the agent-server's existing `buildGatewayPropertyHeaders` pattern. - Tag the commit-message and PR-description helpers via the standard `$ai_span_name` property ("commit_message" / "pr_description") so they're filterable in LLM analytics alongside spans from elsewhere in the system. --- packages/core/src/git-pr/git-pr.test.ts | 7 +++ packages/core/src/git-pr/git-pr.ts | 13 +++++- .../core/src/llm-gateway/llm-gateway.test.ts | 32 +++++++++++++ packages/core/src/llm-gateway/llm-gateway.ts | 46 +++++++++++++++++-- 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/packages/core/src/git-pr/git-pr.test.ts b/packages/core/src/git-pr/git-pr.test.ts index 29f50cc915..869fcdd732 100644 --- a/packages/core/src/git-pr/git-pr.test.ts +++ b/packages/core/src/git-pr/git-pr.test.ts @@ -67,6 +67,9 @@ describe("GitPrService.generateCommitMessage", () => { expect(messages[0].content).toContain("modified: x.ts"); expect(messages[0].content).toContain("why context"); expect(options.system).toContain("commit message generator"); + expect(options.posthogProperties).toEqual({ + $ai_span_name: "commit_message", + }); }); }); @@ -98,6 +101,10 @@ describe("GitPrService.generatePrTitleAndBody", () => { expect(result.title).toBe("feat: add widget"); expect(result.body).toBe("TL;DR: adds a widget."); expect(diffSource.fetchFromRemote).toHaveBeenCalledWith("/repo"); + const [, options] = (llm.prompt as ReturnType).mock.calls[0]; + expect(options.posthogProperties).toEqual({ + $ai_span_name: "pr_description", + }); }); }); diff --git a/packages/core/src/git-pr/git-pr.ts b/packages/core/src/git-pr/git-pr.ts index 3d67958c6f..8291590cf6 100644 --- a/packages/core/src/git-pr/git-pr.ts +++ b/packages/core/src/git-pr/git-pr.ts @@ -100,7 +100,11 @@ ${truncatedDiff}${contextSection}`; const response = await this.llm.prompt( [{ role: "user", content: userMessage }], - { system, model: HELPER_GATEWAY_MODEL }, + { + system, + model: HELPER_GATEWAY_MODEL, + posthogProperties: { $ai_span_name: "commit_message" }, + }, ); return { message: response.content.trim() }; @@ -211,7 +215,12 @@ ${truncatedDiff || "(no diff available)"}${contextSection}`; const response = await this.llm.prompt( [{ role: "user", content: userMessage }], - { system, maxTokens: 2000, model: HELPER_GATEWAY_MODEL }, + { + system, + maxTokens: 2000, + model: HELPER_GATEWAY_MODEL, + posthogProperties: { $ai_span_name: "pr_description" }, + }, ); const content = response.content.trim(); diff --git a/packages/core/src/llm-gateway/llm-gateway.test.ts b/packages/core/src/llm-gateway/llm-gateway.test.ts index 7840442f8c..b333d97697 100644 --- a/packages/core/src/llm-gateway/llm-gateway.test.ts +++ b/packages/core/src/llm-gateway/llm-gateway.test.ts @@ -99,6 +99,38 @@ describe("LlmGatewayService.prompt", () => { expect(body.stream).toBe(false); }); + it("forwards posthogProperties as x-posthog-property-* request headers and skips nulls", async () => { + const fetchMock = vi + .fn() + .mockResolvedValue(createJsonResponse(SUCCESS_BODY)); + const { service } = createService(fetchMock); + + await service.prompt([{ role: "user", content: "hi" }], { + posthogProperties: { + $ai_span_name: "pr_description", + task_id: 42, + is_dry_run: false, + // Null/undefined values are dropped so the gateway doesn't see + // literal "null" strings on the captured event. + unused: null, + skipped: undefined, + // Newlines and non-latin1 bytes are sanitized so an undici-backed + // fetch doesn't reject the request before it's sent. + rich: "line one\nline two — done 🎉", + }, + }); + + const [, init] = fetchMock.mock.calls[0]; + expect(init.headers).toMatchObject({ + "x-posthog-property-$ai_span_name": "pr_description", + "x-posthog-property-task_id": "42", + "x-posthog-property-is_dry_run": "false", + "x-posthog-property-rich": "line one line two done ", + }); + expect(init.headers).not.toHaveProperty("x-posthog-property-unused"); + expect(init.headers).not.toHaveProperty("x-posthog-property-skipped"); + }); + it("throws a typed LlmGatewayError with parsed error fields on non-ok response", async () => { const fetchMock = vi.fn().mockResolvedValue( createJsonResponse( diff --git a/packages/core/src/llm-gateway/llm-gateway.ts b/packages/core/src/llm-gateway/llm-gateway.ts index 13fe496707..e504e54e72 100644 --- a/packages/core/src/llm-gateway/llm-gateway.ts +++ b/packages/core/src/llm-gateway/llm-gateway.ts @@ -21,6 +21,32 @@ import { // the cheapest model rather than the gateway default. export const HELPER_GATEWAY_MODEL = "claude-haiku-4-5"; +/** + * Build `x-posthog-property-` request headers from the caller's + * metadata map. The gateway lifts each header onto the `$ai_generation` + * event it emits — see `services/llm-gateway/src/llm_gateway/request_context.py` + * in posthog/posthog. Null/undefined values are dropped; values are + * sanitized to be HTTP-header safe (newlines collapsed, non-latin1 bytes + * stripped) so an HTTP client like undici doesn't reject the request + * before it's sent. + */ +function buildPosthogPropertyHeaders( + properties: + | Record + | undefined, +): Record { + if (!properties) return {}; + const headers: Record = {}; + for (const [key, value] of Object.entries(properties)) { + if (value === null || value === undefined) continue; + const sanitized = String(value) + .replace(/[\r\n]+/g, " ") + .replace(/[^\x20-\x7e\x80-\xff]/g, ""); + headers[`x-posthog-property-${key}`] = sanitized; + } + return headers; +} + export class LlmGatewayError extends Error { constructor( message: string, @@ -58,6 +84,16 @@ export class LlmGatewayService { model?: string; signal?: AbortSignal; timeoutMs?: number; + /** + * Free-form metadata forwarded as `x-posthog-property-` headers. + * The gateway lifts each one onto the `$ai_generation` event it + * captures, so helper callers (commit messages, PR descriptions, etc.) + * can be told apart from the agent's main generations. + */ + posthogProperties?: Record< + string, + string | number | boolean | null | undefined + >; } = {}, ): Promise { const { @@ -66,6 +102,7 @@ export class LlmGatewayService { model = this.endpoints.defaultModel, signal, timeoutMs = 60_000, + posthogProperties, } = options; const auth = await this.auth.getValidAccessToken(); @@ -101,13 +138,16 @@ export class LlmGatewayService { else signal.addEventListener("abort", onCallerAbort, { once: true }); } + const headers: Record = { + "Content-Type": "application/json", + ...buildPosthogPropertyHeaders(posthogProperties), + }; + let response: Response; try { response = await this.auth.authenticatedFetch(messagesUrl, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers, body: JSON.stringify(requestBody), signal: timeoutController.signal, }); From 380b4334485de195852a0a963b20da85cdea266c Mon Sep 17 00:00:00 2001 From: Vojta Bartos Date: Tue, 30 Jun 2026 10:22:38 +0200 Subject: [PATCH 2/3] refactor(shared): share x-posthog-property header builders Both the agent (ANTHROPIC_CUSTOM_HEADERS) and the new LlmGatewayService (fetch headers) need to turn a property map into x-posthog-property-* headers with the same sanitize rules. Pull the iteration + sanitize into @posthog/shared/posthog-property-headers and expose two thin adapters: buildPosthogPropertyHeaderRecord for fetch and buildPosthogPropertyHeaderLines for the SDK env-var format. --- packages/agent/src/utils/gateway.test.ts | 78 ----------- packages/agent/src/utils/gateway.ts | 32 +---- packages/core/src/llm-gateway/llm-gateway.ts | 39 ++---- packages/shared/package.json | 4 + .../src/posthog-property-headers.test.ts | 130 ++++++++++++++++++ .../shared/src/posthog-property-headers.ts | 61 ++++++++ packages/shared/tsup.config.ts | 1 + 7 files changed, 205 insertions(+), 140 deletions(-) create mode 100644 packages/shared/src/posthog-property-headers.test.ts create mode 100644 packages/shared/src/posthog-property-headers.ts diff --git a/packages/agent/src/utils/gateway.test.ts b/packages/agent/src/utils/gateway.test.ts index 9a0b06e3e7..2320e7f452 100644 --- a/packages/agent/src/utils/gateway.test.ts +++ b/packages/agent/src/utils/gateway.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; import { - buildGatewayPropertyHeaders, getLlmGatewayUrl, resolveGatewayProduct, resolveLlmGatewayUrl, @@ -60,83 +59,6 @@ describe("resolveGatewayProduct", () => { ); }); -describe("buildGatewayPropertyHeaders", () => { - it("renders each property as an x-posthog-property header line", () => { - expect( - buildGatewayPropertyHeaders({ - task_origin_product: "signal_report", - task_internal: true, - }), - ).toBe( - "x-posthog-property-task_origin_product: signal_report\nx-posthog-property-task_internal: true", - ); - }); - - it("drops null and undefined values but keeps falsy primitives", () => { - expect( - buildGatewayPropertyHeaders({ - task_origin_product: null, - task_internal: false, - task_count: 0, - }), - ).toBe( - "x-posthog-property-task_internal: false\nx-posthog-property-task_count: 0", - ); - }); - - it("returns an empty string when no usable properties remain", () => { - expect( - buildGatewayPropertyHeaders({ - task_origin_product: null, - task_internal: undefined, - }), - ).toBe(""); - }); - - it.each([ - { - description: "LF", - title: "Fix the bug\nx-posthog-property-task_internal: true", - }, - { - description: "CRLF", - title: "Fix the bug\r\nx-posthog-property-task_internal: true", - }, - { - description: "CR", - title: "Fix the bug\rx-posthog-property-task_internal: true", - }, - { - description: "consecutive newlines", - title: "Fix the bug\n\nx-posthog-property-task_internal: true", - }, - ])( - "collapses $description in values so they cannot inject extra headers", - ({ title }) => { - expect( - buildGatewayPropertyHeaders({ - task_title: title, - task_id: "task-abc", - }), - ).toBe( - "x-posthog-property-task_title: Fix the bug x-posthog-property-task_internal: true\nx-posthog-property-task_id: task-abc", - ); - }, - ); - - it("strips characters an HTTP header value cannot carry", () => { - expect(buildGatewayPropertyHeaders({ task_title: "don’t🚀ship" })).toBe( - "x-posthog-property-task_title: dontship", - ); - }); - - it("keeps latin1 characters such as accents", () => { - expect(buildGatewayPropertyHeaders({ task_title: "café" })).toBe( - "x-posthog-property-task_title: café", - ); - }); -}); - describe("resolveLlmGatewayUrl", () => { it("appends the product slug to an env-provided base URL", () => { expect( diff --git a/packages/agent/src/utils/gateway.ts b/packages/agent/src/utils/gateway.ts index ef39ec2bb5..7f024fbb88 100644 --- a/packages/agent/src/utils/gateway.ts +++ b/packages/agent/src/utils/gateway.ts @@ -27,37 +27,7 @@ export function resolveGatewayProduct({ return "posthog_code"; } -/** - * Make a value safe to embed in an HTTP header value. Collapses newlines to - * spaces (the header block is newline-delimited) and drops characters outside - * the valid header-byte range — control chars and code points above latin1 - * (emoji, smart quotes) — which an HTTP client (e.g. undici) would otherwise - * reject before sending. ASCII is preserved. - */ -function sanitizeHeaderValue(value: string): string { - return value.replace(/[\r\n]+/g, " ").replace(/[^\x20-\x7e\x80-\xff]/g, ""); -} - -/** - * Build `x-posthog-property-: ` header lines that the LLM - * gateway lifts onto the `$ai_generation` event it captures for each call - * (see `services/llm-gateway/src/llm_gateway/request_context.py`). - * - * Returns a newline-joined string ready for `ANTHROPIC_CUSTOM_HEADERS`. - * `null`/`undefined` values are dropped; values are sanitized to be HTTP-header - * safe (see {@link sanitizeHeaderValue}). - */ -export function buildGatewayPropertyHeaders( - properties: Record, -): string { - return Object.entries(properties) - .filter(([, value]) => value !== null && value !== undefined) - .map( - ([key, value]) => - `x-posthog-property-${key}: ${sanitizeHeaderValue(String(value))}`, - ) - .join("\n"); -} +export { buildPosthogPropertyHeaderLines as buildGatewayPropertyHeaders } from "@posthog/shared/posthog-property-headers"; function getGatewayBaseUrl(posthogHost: string): string { const url = new URL(posthogHost); diff --git a/packages/core/src/llm-gateway/llm-gateway.ts b/packages/core/src/llm-gateway/llm-gateway.ts index e504e54e72..52b3d15121 100644 --- a/packages/core/src/llm-gateway/llm-gateway.ts +++ b/packages/core/src/llm-gateway/llm-gateway.ts @@ -1,4 +1,8 @@ import { ROOT_LOGGER, type RootLogger } from "@posthog/di/logger"; +import { + buildPosthogPropertyHeaderRecord, + type PosthogProperties, +} from "@posthog/shared/posthog-property-headers"; import { inject, injectable } from "inversify"; import { LLM_GATEWAY_HOST, @@ -21,32 +25,6 @@ import { // the cheapest model rather than the gateway default. export const HELPER_GATEWAY_MODEL = "claude-haiku-4-5"; -/** - * Build `x-posthog-property-` request headers from the caller's - * metadata map. The gateway lifts each header onto the `$ai_generation` - * event it emits — see `services/llm-gateway/src/llm_gateway/request_context.py` - * in posthog/posthog. Null/undefined values are dropped; values are - * sanitized to be HTTP-header safe (newlines collapsed, non-latin1 bytes - * stripped) so an HTTP client like undici doesn't reject the request - * before it's sent. - */ -function buildPosthogPropertyHeaders( - properties: - | Record - | undefined, -): Record { - if (!properties) return {}; - const headers: Record = {}; - for (const [key, value] of Object.entries(properties)) { - if (value === null || value === undefined) continue; - const sanitized = String(value) - .replace(/[\r\n]+/g, " ") - .replace(/[^\x20-\x7e\x80-\xff]/g, ""); - headers[`x-posthog-property-${key}`] = sanitized; - } - return headers; -} - export class LlmGatewayError extends Error { constructor( message: string, @@ -90,10 +68,7 @@ export class LlmGatewayService { * captures, so helper callers (commit messages, PR descriptions, etc.) * can be told apart from the agent's main generations. */ - posthogProperties?: Record< - string, - string | number | boolean | null | undefined - >; + posthogProperties?: PosthogProperties; } = {}, ): Promise { const { @@ -140,7 +115,9 @@ export class LlmGatewayService { const headers: Record = { "Content-Type": "application/json", - ...buildPosthogPropertyHeaders(posthogProperties), + ...(posthogProperties + ? buildPosthogPropertyHeaderRecord(posthogProperties) + : {}), }; let response: Response; diff --git a/packages/shared/package.json b/packages/shared/package.json index 3f0a2c8d02..7a1ee01192 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -39,6 +39,10 @@ "./mcp-sandbox-proxy": { "types": "./dist/mcp-sandbox-proxy.d.ts", "import": "./dist/mcp-sandbox-proxy.js" + }, + "./posthog-property-headers": { + "types": "./dist/posthog-property-headers.d.ts", + "import": "./dist/posthog-property-headers.js" } }, "scripts": { diff --git a/packages/shared/src/posthog-property-headers.test.ts b/packages/shared/src/posthog-property-headers.test.ts new file mode 100644 index 0000000000..ca53352093 --- /dev/null +++ b/packages/shared/src/posthog-property-headers.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from "vitest"; +import { + buildPosthogPropertyHeaderLines, + buildPosthogPropertyHeaderRecord, +} from "./posthog-property-headers"; + +describe("buildPosthogPropertyHeaderRecord", () => { + it("returns each property as an x-posthog-property- entry", () => { + expect( + buildPosthogPropertyHeaderRecord({ + task_origin_product: "signal_report", + task_internal: true, + }), + ).toEqual({ + "x-posthog-property-task_origin_product": "signal_report", + "x-posthog-property-task_internal": "true", + }); + }); + + it("drops null and undefined values but keeps falsy primitives", () => { + expect( + buildPosthogPropertyHeaderRecord({ + task_origin_product: null, + task_internal: false, + task_count: 0, + skipped: undefined, + }), + ).toEqual({ + "x-posthog-property-task_internal": "false", + "x-posthog-property-task_count": "0", + }); + }); + + it("returns an empty record when no usable properties remain", () => { + expect( + buildPosthogPropertyHeaderRecord({ + task_origin_product: null, + task_internal: undefined, + }), + ).toEqual({}); + }); + + it("collapses newline variants so a value cannot inject extra headers", () => { + expect( + buildPosthogPropertyHeaderRecord({ + task_title: "Fix the bug\r\nx-posthog-property-injected: true", + }), + ).toEqual({ + "x-posthog-property-task_title": + "Fix the bug x-posthog-property-injected: true", + }); + }); + + it("strips characters an HTTP header value cannot carry", () => { + expect( + buildPosthogPropertyHeaderRecord({ task_title: "don’t🚀ship" }), + ).toEqual({ "x-posthog-property-task_title": "dontship" }); + }); + + it("keeps latin1 characters such as accents", () => { + expect( + buildPosthogPropertyHeaderRecord({ task_title: "café" }), + ).toEqual({ "x-posthog-property-task_title": "café" }); + }); +}); + +describe("buildPosthogPropertyHeaderLines", () => { + it("renders each property as an x-posthog-property header line", () => { + expect( + buildPosthogPropertyHeaderLines({ + task_origin_product: "signal_report", + task_internal: true, + }), + ).toBe( + "x-posthog-property-task_origin_product: signal_report\nx-posthog-property-task_internal: true", + ); + }); + + it("drops null and undefined values but keeps falsy primitives", () => { + expect( + buildPosthogPropertyHeaderLines({ + task_origin_product: null, + task_internal: false, + task_count: 0, + }), + ).toBe( + "x-posthog-property-task_internal: false\nx-posthog-property-task_count: 0", + ); + }); + + it("returns an empty string when no usable properties remain", () => { + expect( + buildPosthogPropertyHeaderLines({ + task_origin_product: null, + task_internal: undefined, + }), + ).toBe(""); + }); + + it.each([ + { + description: "LF", + title: "Fix the bug\nx-posthog-property-task_internal: true", + }, + { + description: "CRLF", + title: "Fix the bug\r\nx-posthog-property-task_internal: true", + }, + { + description: "CR", + title: "Fix the bug\rx-posthog-property-task_internal: true", + }, + { + description: "consecutive newlines", + title: "Fix the bug\n\nx-posthog-property-task_internal: true", + }, + ])( + "collapses $description in values so they cannot inject extra headers", + ({ title }) => { + expect( + buildPosthogPropertyHeaderLines({ + task_title: title, + task_id: "task-abc", + }), + ).toBe( + "x-posthog-property-task_title: Fix the bug x-posthog-property-task_internal: true\nx-posthog-property-task_id: task-abc", + ); + }, + ); +}); diff --git a/packages/shared/src/posthog-property-headers.ts b/packages/shared/src/posthog-property-headers.ts new file mode 100644 index 0000000000..936763ed68 --- /dev/null +++ b/packages/shared/src/posthog-property-headers.ts @@ -0,0 +1,61 @@ +export type PosthogPropertyValue = + | string + | number + | boolean + | null + | undefined; + +export type PosthogProperties = Record; + +/** + * Make a value safe to embed in an HTTP header value. Collapses newlines to + * spaces (the header block is newline-delimited) and drops characters outside + * the valid header-byte range — control chars and code points above latin1 + * (emoji, smart quotes) — which an HTTP client (e.g. undici) would otherwise + * reject before sending. ASCII is preserved. + */ +function sanitizeHeaderValue(value: string): string { + return value.replace(/[\r\n]+/g, " ").replace(/[^\x20-\x7e\x80-\xff]/g, ""); +} + +function buildEntries( + properties: PosthogProperties, +): Array<[string, string]> { + const entries: Array<[string, string]> = []; + for (const [key, value] of Object.entries(properties)) { + if (value === null || value === undefined) continue; + entries.push([ + `x-posthog-property-${key}`, + sanitizeHeaderValue(String(value)), + ]); + } + return entries; +} + +/** + * Build a `Record` of `x-posthog-property-` headers + * suitable for `fetch()` init.headers. The LLM gateway lifts each header + * onto the `$ai_generation` event it captures + * (see `services/llm-gateway/src/llm_gateway/request_context.py` in + * posthog/posthog). `null`/`undefined` values are dropped; values are + * sanitized via {@link sanitizeHeaderValue}. + */ +export function buildPosthogPropertyHeaderRecord( + properties: PosthogProperties, +): Record { + return Object.fromEntries(buildEntries(properties)); +} + +/** + * Same property semantics as {@link buildPosthogPropertyHeaderRecord}, but + * returns a newline-joined string of `key: value` lines — the format + * `ANTHROPIC_CUSTOM_HEADERS` expects when wiring headers into the Claude + * Agent SDK. + */ +export function buildPosthogPropertyHeaderLines( + properties: PosthogProperties, +): string { + return buildEntries(properties) + .map(([key, value]) => `${key}: ${value}`) + .join("\n"); +} diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts index 742102c8e1..4d3231a785 100644 --- a/packages/shared/tsup.config.ts +++ b/packages/shared/tsup.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ "src/dismissalReasons.ts", "src/domain-types.ts", "src/mcp-sandbox-proxy.ts", + "src/posthog-property-headers.ts", "src/types.ts", ], format: ["esm"], From a6f79a1254993d3803c3cd7eeb1977e9c5da231d Mon Sep 17 00:00:00 2001 From: Vojta Bartos Date: Tue, 30 Jun 2026 10:25:09 +0200 Subject: [PATCH 3/3] chore(shared): biome format posthog-property-headers --- packages/shared/src/posthog-property-headers.test.ts | 6 +++--- packages/shared/src/posthog-property-headers.ts | 11 ++--------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/shared/src/posthog-property-headers.test.ts b/packages/shared/src/posthog-property-headers.test.ts index ca53352093..6b93ffc50e 100644 --- a/packages/shared/src/posthog-property-headers.test.ts +++ b/packages/shared/src/posthog-property-headers.test.ts @@ -58,9 +58,9 @@ describe("buildPosthogPropertyHeaderRecord", () => { }); it("keeps latin1 characters such as accents", () => { - expect( - buildPosthogPropertyHeaderRecord({ task_title: "café" }), - ).toEqual({ "x-posthog-property-task_title": "café" }); + expect(buildPosthogPropertyHeaderRecord({ task_title: "café" })).toEqual({ + "x-posthog-property-task_title": "café", + }); }); }); diff --git a/packages/shared/src/posthog-property-headers.ts b/packages/shared/src/posthog-property-headers.ts index 936763ed68..8430a61df5 100644 --- a/packages/shared/src/posthog-property-headers.ts +++ b/packages/shared/src/posthog-property-headers.ts @@ -1,9 +1,4 @@ -export type PosthogPropertyValue = - | string - | number - | boolean - | null - | undefined; +export type PosthogPropertyValue = string | number | boolean | null | undefined; export type PosthogProperties = Record; @@ -18,9 +13,7 @@ function sanitizeHeaderValue(value: string): string { return value.replace(/[\r\n]+/g, " ").replace(/[^\x20-\x7e\x80-\xff]/g, ""); } -function buildEntries( - properties: PosthogProperties, -): Array<[string, string]> { +function buildEntries(properties: PosthogProperties): Array<[string, string]> { const entries: Array<[string, string]> = []; for (const [key, value] of Object.entries(properties)) { if (value === null || value === undefined) continue;