From a8c5f14037ba5bb960aa966f8c7eef264194bf4b Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 15 May 2026 19:02:37 +0200 Subject: [PATCH 1/6] Posthog: drop $initial_person_info from outgoing events --- src/analytics/PosthogAnalytics.test.ts | 42 ++++++++++++++++++++++++++ src/analytics/PosthogAnalytics.ts | 3 ++ 2 files changed, 45 insertions(+) diff --git a/src/analytics/PosthogAnalytics.test.ts b/src/analytics/PosthogAnalytics.test.ts index 49af5eae9a..096021c7f9 100644 --- a/src/analytics/PosthogAnalytics.test.ts +++ b/src/analytics/PosthogAnalytics.test.ts @@ -14,6 +14,7 @@ import { beforeAll, afterAll, } from "vitest"; +import posthog, { type Properties } from "posthog-js"; import { PosthogAnalytics } from "./PosthogAnalytics"; import { mockConfig } from "../utils/test"; @@ -88,4 +89,45 @@ describe("PosthogAnalytics", () => { expect(PosthogAnalytics.instance.isEnabled()).toBe(true); }); }); + + describe("sanitizeProperties", () => { + beforeAll(() => { + vi.stubEnv("VITE_PACKAGE", "full"); + }); + + beforeEach(() => { + mockConfig({ + posthog: { + api_host: "https://api.example.com.localhost", + api_key: "api_key", + }, + }); + PosthogAnalytics.resetInstance(); + }); + + afterAll(() => { + vi.unstubAllEnvs(); + }); + + it("drops $initial_person_info from event properties", () => { + const initSpy = vi.spyOn(posthog, "init"); + expect(PosthogAnalytics.instance.isEnabled()).toBe(true); + + const sanitize = initSpy.mock.calls[0][1]?.sanitize_properties; + expect(sanitize).toBeDefined(); + + const sanitized = sanitize!( + { + $current_url: "https://call.example.com/some/private/path", + $initial_person_info: { + r: "https://example.com/referrer", + u: "https://call.example.com/some/private/path", + }, + } as Properties, + "anyEvent", + ); + + expect(sanitized).not.toHaveProperty("$initial_person_info"); + }); + }); }); diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 6ec8f8c76d..0ec67019d7 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -173,6 +173,9 @@ export class PosthogAnalytics { .slice(0, 3) .join(""); + // drop $initial_person_info for increased privacy. + delete properties["$initial_person_info"]; + return properties; }; From a08a561c4ccbc052c6a01b893b377c8fa64d9c95 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 18 May 2026 15:51:49 +0200 Subject: [PATCH 2/6] Posthog: migrate from sanitize_properties to before_send --- package.json | 2 +- pnpm-lock.yaml | 298 +++++++++++++++++++++++-- src/analytics/PosthogAnalytics.test.ts | 88 ++++++-- src/analytics/PosthogAnalytics.ts | 75 ++++--- 4 files changed, 399 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index fef415eb93..f4e178477d 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "pako": "^2.0.4", "postcss": "^8.4.41", "postcss-preset-env": "^10.0.0", - "posthog-js": "1.160.3", + "posthog-js": "1.374.0", "prettier": "^3.0.0", "qrcode": "^1.5.4", "react": "19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35fbc66f61..7d797ee7d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -259,8 +259,8 @@ importers: specifier: ^10.0.0 version: 10.6.1(postcss@8.5.11) posthog-js: - specifier: 1.160.3 - version: 1.160.3 + specifier: 1.374.0 + version: 1.374.0 prettier: specifier: ^3.0.0 version: 3.8.3 @@ -329,7 +329,7 @@ importers: version: 3.6.0(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) vitest: specifier: ^4.0.18 - version: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) vitest-axe: specifier: ^1.0.0-pre.3 version: 1.0.0-pre.5(vitest@4.1.5) @@ -1666,6 +1666,78 @@ packages: '@octokit/types@13.10.0': resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.7.1': + resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-http@0.208.0': + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.208.0': + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.208.0': + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.2.0': + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.7.1': + resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.208.0': + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.2.0': + resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.2.0': + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} @@ -1874,6 +1946,42 @@ packages: engines: {node: '>=18'} hasBin: true + '@posthog/core@1.29.3': + resolution: {integrity: sha512-OvJSAzqVfZx+L7D874q56FVRTxOIsFBVB3wSB/Uny+DhmfNRGDi1rpZAruEmQYl9WQlQJb1q6JXGAC+rxVXjPA==} + + '@posthog/types@1.374.0': + resolution: {integrity: sha512-qouREpHIxsBS3Gc6a5gZvg6/ykK+4TJAs4wYTUIH/emH1HQfaaLrWzGoEm+/OPwlNxHzw4tQn9OOyxsmr9NF2g==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2953,6 +3061,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -3693,6 +3804,9 @@ packages: core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3915,6 +4029,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.5: + resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -5053,6 +5170,9 @@ packages: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -5597,8 +5717,8 @@ packages: resolution: {integrity: sha512-5dDj8+lmvA8XB78SmzGI8NlQoksv7IfutGWeVZxiixHbO+p4LDPT3wuG/D9sM/wrjZZ9I+Siy/e117vbFPxSZg==} engines: {node: ^10 || ^12 || >=14} - posthog-js@1.160.3: - resolution: {integrity: sha512-mGvxOIlWPtdPx8EI0MQ81wNKlnH2K0n4RqwQOl044b34BCKiFVzZ7Hc7geMuZNaRAvCi5/5zyGeWHcAYZQxiMQ==} + posthog-js@1.374.0: + resolution: {integrity: sha512-3M2xsHXU7Hl64KGZjljq13jIKiJ4N7npY1n+1Q7VQmQKdVsoTc9geaeoHprZEZCMXp3b2qbWZEvIYjekUN5lAg==} preact@10.29.1: resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} @@ -5638,6 +5758,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + protobufjs@7.5.9: + resolution: {integrity: sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==} + engines: {node: '>=12.0.0'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -5660,6 +5784,9 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + querystring-es3@0.2.1: resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} engines: {node: '>=0.4.x'} @@ -6683,8 +6810,8 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} - web-vitals@4.2.4: - resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + web-vitals@5.2.0: + resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==} webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -8338,6 +8465,82 @@ snapshots: dependencies: '@octokit/openapi-types': 24.2.0 + '@opentelemetry/api-logs@0.208.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.1) + protobufjs: 7.5.9 + + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/semantic-conventions@1.41.1': {} + '@oxc-project/types@0.127.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': @@ -8473,6 +8676,34 @@ snapshots: dependencies: playwright: 1.59.1 + '@posthog/core@1.29.3': + dependencies: + '@posthog/types': 1.374.0 + + '@posthog/types@1.374.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -9446,6 +9677,9 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/uuid@10.0.0': {} '@types/yargs-parser@21.0.3': {} @@ -9757,7 +9991,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) '@vitest/expect@3.2.4': dependencies: @@ -10343,6 +10577,8 @@ snapshots: dependencies: browserslist: 4.28.2 + core-js@3.49.0: {} + core-util-is@1.0.3: {} cosmiconfig@8.3.6(typescript@5.9.3): @@ -10575,6 +10811,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.5: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 @@ -11945,6 +12185,8 @@ snapshots: loglevel@1.9.2: {} + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -12616,11 +12858,21 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - posthog-js@1.160.3: - dependencies: + posthog-js@1.374.0: + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.1) + '@posthog/core': 1.29.3 + '@posthog/types': 1.374.0 + core-js: 3.49.0 + dompurify: 3.4.5 fflate: 0.4.8 preact: 10.29.1 - web-vitals: 4.2.4 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.2.0 preact@10.29.1: {} @@ -12653,6 +12905,21 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + protobufjs@7.5.9: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 24.12.2 + long: 5.3.2 + proxy-from-env@1.1.0: {} public-encrypt@4.0.3: @@ -12678,6 +12945,8 @@ snapshots: dependencies: side-channel: 1.1.0 + query-selector-shadow-dom@1.0.1: {} + querystring-es3@0.2.1: {} queue-microtask@1.2.3: {} @@ -13788,9 +14057,9 @@ snapshots: axe-core: 4.11.3 chalk: 5.6.2 lodash-es: 4.18.1 - vitest: 4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) - vitest@4.1.5(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + vitest@4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0)(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.5 '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -13813,6 +14082,7 @@ snapshots: vite: 8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 24.12.2 '@vitest/coverage-v8': 4.1.5(vitest@4.1.5) jsdom: 26.1.0 @@ -13836,7 +14106,7 @@ snapshots: walk-up-path@4.0.0: {} - web-vitals@4.2.4: {} + web-vitals@5.2.0: {} webidl-conversions@3.0.1: {} diff --git a/src/analytics/PosthogAnalytics.test.ts b/src/analytics/PosthogAnalytics.test.ts index 096021c7f9..8196fa4dd6 100644 --- a/src/analytics/PosthogAnalytics.test.ts +++ b/src/analytics/PosthogAnalytics.test.ts @@ -14,9 +14,13 @@ import { beforeAll, afterAll, } from "vitest"; -import posthog, { type Properties } from "posthog-js"; +import posthog, { type CaptureResult } from "posthog-js"; -import { PosthogAnalytics } from "./PosthogAnalytics"; +import { + Anonymity, + applyPrivacyFilters, + PosthogAnalytics, +} from "./PosthogAnalytics"; import { mockConfig } from "../utils/test"; describe("PosthogAnalytics", () => { @@ -90,7 +94,57 @@ describe("PosthogAnalytics", () => { }); }); - describe("sanitizeProperties", () => { + describe("applyPrivacyFilters", () => { + const makeEvent = (properties: Record): CaptureResult => + ({ event: "anyEvent", properties }) as unknown as CaptureResult; + + it("drops $initial_person_info regardless of anonymity", () => { + const out = applyPrivacyFilters( + makeEvent({ + $current_url: "https://call.example.com/some/private/path", + $initial_person_info: { + r: "https://example.com/referrer", + u: "https://call.example.com/some/private/path", + }, + }), + Anonymity.Pseudonymous, + ); + expect(out?.properties).not.toHaveProperty("$initial_person_info"); + }); + + it("strips path from $current_url", () => { + const out = applyPrivacyFilters( + makeEvent({ $current_url: "https://call.example.com/x/y/z" }), + Anonymity.Pseudonymous, + ); + expect(out?.properties["$current_url"]).not.toContain("/x/y/z"); + }); + + it("nulls referrer and device fields when anonymous", () => { + const out = applyPrivacyFilters( + makeEvent({ + $current_url: "https://x/y", + $referrer: "https://leaky", + $initial_referrer: "https://leaky-too", + $device_id: "uuid", + }), + Anonymity.Anonymous, + ); + expect(out?.properties["$referrer"]).toBeNull(); + expect(out?.properties["$initial_referrer"]).toBeNull(); + expect(out?.properties["$device_id"]).toBeNull(); + }); + + it("passes null events through unchanged", () => { + expect(applyPrivacyFilters(null, Anonymity.Pseudonymous)).toBeNull(); + }); + }); + + // Verifies that applyPrivacyFilters is actually wired into posthog.init via + // the before_send hook — guards against typos in the option name or future + // posthog-js bumps renaming/removing the hook. The filter logic itself is + // covered by the applyPrivacyFilters block above. + describe("posthog.init wiring", () => { beforeAll(() => { vi.stubEnv("VITE_PACKAGE", "full"); }); @@ -109,25 +163,25 @@ describe("PosthogAnalytics", () => { vi.unstubAllEnvs(); }); - it("drops $initial_person_info from event properties", () => { + it("passes events through the privacy filter via before_send", () => { const initSpy = vi.spyOn(posthog, "init"); expect(PosthogAnalytics.instance.isEnabled()).toBe(true); - const sanitize = initSpy.mock.calls[0][1]?.sanitize_properties; - expect(sanitize).toBeDefined(); + const beforeSend = initSpy.mock.calls[0][1]?.before_send; + expect(beforeSend).toBeInstanceOf(Function); - const sanitized = sanitize!( - { - $current_url: "https://call.example.com/some/private/path", - $initial_person_info: { - r: "https://example.com/referrer", - u: "https://call.example.com/some/private/path", - }, - } as Properties, - "anyEvent", - ); + const event = { + event: "anyEvent", + properties: { + $current_url: "https://call.example.com/x/y", + $initial_person_info: { r: "x" }, + }, + } as unknown as CaptureResult; - expect(sanitized).not.toHaveProperty("$initial_person_info"); + const out = (beforeSend as (e: CaptureResult) => CaptureResult | null)( + event, + ); + expect(out?.properties).not.toHaveProperty("$initial_person_info"); }); }); }); diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 0ec67019d7..08b275053a 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details. import posthog, { type CaptureOptions, + type CaptureResult, type PostHog, type Properties, } from "posthog-js"; @@ -65,6 +66,45 @@ export enum RegistrationType { Registered, } +/** + * Strip PII from posthog's built-in properties (URL, referrer fields, + * device ID, $initial_person_info) before events leave the client. + * See src/utils/event-utils.ts in posthog-js (getEventProperties, getPersonInfo) + * for the list of properties posthog sets automatically. + */ +export function applyPrivacyFilters( + event: CaptureResult | null, + anonymity: Anonymity, +): CaptureResult | null { + if (event === null) return null; + const properties = event.properties; + + if (anonymity === Anonymity.Anonymous) { + // drop referrer information for anonymous users + properties["$referrer"] = null; + properties["$referring_domain"] = null; + properties["$initial_referrer"] = null; + properties["$initial_referring_domain"] = null; + + // drop device ID, which is a UUID persisted in local storage + properties["$device_id"] = null; + } + + // the url leaks a lot of private data like the call name or the user. + // Its stripped down to the bare minimum to only give insights about the host (develop, main or sfu) + if (typeof properties["$current_url"] === "string") { + properties["$current_url"] = properties["$current_url"] + .split("/") + .slice(0, 3) + .join(""); + } + + // drop $initial_person_info for increased privacy. + delete properties["$initial_person_info"]; + + return event; +} + interface PlatformProperties { appVersion: string; matrixBackend: "embedded" | "jssdk"; @@ -129,13 +169,15 @@ export class PosthogAnalytics { } if (apiKey && apiHost) { + const beforeSend = (event: CaptureResult | null): CaptureResult | null => + applyPrivacyFilters(event, this.anonymity); this.posthog.init(apiKey, { api_host: apiHost, autocapture: false, mask_all_text: true, mask_all_element_attributes: true, capture_pageview: false, - sanitize_properties: this.sanitizeProperties, + before_send: beforeSend, respect_dnt: true, advanced_disable_decide: true, }); @@ -148,37 +190,6 @@ export class PosthogAnalytics { } } - private sanitizeProperties = ( - properties: Properties, - _eventName: string, - ): Properties => { - // Callback from posthog to sanitize properties before sending them to the server. - // Here we sanitize posthog's built in properties which leak PII e.g. url reporting. - // See utils.js _.info.properties in posthog-js. - - if (this.anonymity == Anonymity.Anonymous) { - // drop referrer information for anonymous users - properties["$referrer"] = null; - properties["$referring_domain"] = null; - properties["$initial_referrer"] = null; - properties["$initial_referring_domain"] = null; - - // drop device ID, which is a UUID persisted in local storage - properties["$device_id"] = null; - } - // the url leaks a lot of private data like the call name or the user. - // Its stripped down to the bare minimum to only give insights about the host (develop, main or sfu) - properties["$current_url"] = (properties["$current_url"] as string) - .split("/") - .slice(0, 3) - .join(""); - - // drop $initial_person_info for increased privacy. - delete properties["$initial_person_info"]; - - return properties; - }; - private registerSuperProperties(properties: Properties): void { if (this.enabled) { this.posthog.register(properties); From e55c15ccb8128f97ba757fd80746a5e1af699531 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 18 May 2026 18:37:27 +0200 Subject: [PATCH 3/6] strip URL fields from $set / $set_once --- src/analytics/PosthogAnalytics.test.ts | 59 +++++++++++++++++++++ src/analytics/PosthogAnalytics.ts | 71 +++++++++++++++++--------- 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/src/analytics/PosthogAnalytics.test.ts b/src/analytics/PosthogAnalytics.test.ts index 8196fa4dd6..b5052840ca 100644 --- a/src/analytics/PosthogAnalytics.test.ts +++ b/src/analytics/PosthogAnalytics.test.ts @@ -138,6 +138,65 @@ describe("PosthogAnalytics", () => { it("passes null events through unchanged", () => { expect(applyPrivacyFilters(null, Anonymity.Pseudonymous)).toBeNull(); }); + + it("strips URL fields nested inside $set_once", () => { + const secretUrl = + "https://call.example.com/room/#/?password=hunter2&roomId=abc"; + const out = applyPrivacyFilters( + makeEvent({ + $current_url: "https://call.example.com/x", + $set_once: { + $current_url: secretUrl, + $initial_current_url: secretUrl, + $session_entry_url: secretUrl, + $initial_person_info: { r: "x", u: secretUrl }, + }, + }), + Anonymity.Pseudonymous, + ); + + const setOnce = out?.properties["$set_once"] as Record; + expect(setOnce["$current_url"]).not.toContain("password"); + expect(setOnce["$initial_current_url"]).not.toContain("password"); + expect(setOnce).not.toHaveProperty("$session_entry_url"); + expect(setOnce).not.toHaveProperty("$initial_person_info"); + }); + + it("strips URL fields nested inside $set", () => { + const secretUrl = + "https://call.example.com/room/#/?password=hunter2&roomId=abc"; + const out = applyPrivacyFilters( + makeEvent({ + $current_url: "https://call.example.com/x", + $set: { + $current_url: secretUrl, + $session_entry_url: secretUrl, + }, + }), + Anonymity.Pseudonymous, + ); + + const set = out?.properties["$set"] as Record; + expect(set["$current_url"]).not.toContain("password"); + expect(set).not.toHaveProperty("$session_entry_url"); + }); + + it("nulls referrer fields inside $set_once when anonymous", () => { + const out = applyPrivacyFilters( + makeEvent({ + $current_url: "https://x/y", + $set_once: { + $initial_referrer: "https://leaky", + $initial_referring_domain: "leaky", + }, + }), + Anonymity.Anonymous, + ); + + const setOnce = out?.properties["$set_once"] as Record; + expect(setOnce["$initial_referrer"]).toBeNull(); + expect(setOnce["$initial_referring_domain"]).toBeNull(); + }); }); // Verifies that applyPrivacyFilters is actually wired into posthog.init via diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 08b275053a..ea234a0a55 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -66,9 +66,47 @@ export enum RegistrationType { Registered, } +// Sanitize URL / referrer / device fields on a single posthog properties bag. +// Applied to event.properties and to the person-profile bags ($set / $set_once), +// since posthog mirrors the same URL fields into those. +function stripPrivacyFields( + obj: Properties | undefined, + anonymity: Anonymity, +): void { + if (!obj) return; + + if (anonymity === Anonymity.Anonymous) { + // drop referrer information for anonymous users + obj["$referrer"] = null; + obj["$referring_domain"] = null; + obj["$initial_referrer"] = null; + obj["$initial_referring_domain"] = null; + + // drop device ID, which is a UUID persisted in local storage + obj["$device_id"] = null; + } + + // the url leaks a lot of private data like the call name or the user + // (room password / room ID can land in the hash/query). Strip down to + // scheme + host so we still get host-level insights (develop / main / sfu). + for (const key of ["$current_url", "$initial_current_url"]) { + if (typeof obj[key] === "string") { + obj[key] = (obj[key] as string).split("/").slice(0, 3).join(""); + } + } + + // $session_entry_url carries the full untrimmed URL; $initial_person_info + // bundles initial referrer + URL into a nested object that bypasses the + // per-key strips above. Drop both. + delete obj["$session_entry_url"]; + delete obj["$initial_person_info"]; +} + /** * Strip PII from posthog's built-in properties (URL, referrer fields, - * device ID, $initial_person_info) before events leave the client. + * device ID, $initial_person_info, $session_entry_url) before events leave + * the client. Also applied to the person-profile bags ($set / $set_once), + * which mirror the same URL fields. * See src/utils/event-utils.ts in posthog-js (getEventProperties, getPersonInfo) * for the list of properties posthog sets automatically. */ @@ -77,30 +115,15 @@ export function applyPrivacyFilters( anonymity: Anonymity, ): CaptureResult | null { if (event === null) return null; - const properties = event.properties; - - if (anonymity === Anonymity.Anonymous) { - // drop referrer information for anonymous users - properties["$referrer"] = null; - properties["$referring_domain"] = null; - properties["$initial_referrer"] = null; - properties["$initial_referring_domain"] = null; - - // drop device ID, which is a UUID persisted in local storage - properties["$device_id"] = null; - } - - // the url leaks a lot of private data like the call name or the user. - // Its stripped down to the bare minimum to only give insights about the host (develop, main or sfu) - if (typeof properties["$current_url"] === "string") { - properties["$current_url"] = properties["$current_url"] - .split("/") - .slice(0, 3) - .join(""); - } - // drop $initial_person_info for increased privacy. - delete properties["$initial_person_info"]; + stripPrivacyFields(event.properties, anonymity); + // posthog can stash person-profile updates either at the top level + // of CaptureResult or nested inside properties depending on the pipeline + // stage; clean both spots so nothing slips through. + stripPrivacyFields(event.$set, anonymity); + stripPrivacyFields(event.$set_once, anonymity); + stripPrivacyFields(event.properties["$set"], anonymity); + stripPrivacyFields(event.properties["$set_once"], anonymity); return event; } From 7354d366fdfb9a9f95d599c105ce81ccd7888ac5 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 18 May 2026 18:37:44 +0200 Subject: [PATCH 4/6] enable mask_personal_data_properties --- src/analytics/PosthogAnalytics.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index ea234a0a55..71ac5cc75d 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -199,6 +199,7 @@ export class PosthogAnalytics { autocapture: false, mask_all_text: true, mask_all_element_attributes: true, + mask_personal_data_properties: true, capture_pageview: false, before_send: beforeSend, respect_dnt: true, From 3032c87b94c5afe5f6c0636c7240b93b638ac9a5 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 19 May 2026 17:32:09 +0200 Subject: [PATCH 5/6] review --- src/analytics/PosthogAnalytics.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 71ac5cc75d..915a6f2d0b 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -69,7 +69,7 @@ export enum RegistrationType { // Sanitize URL / referrer / device fields on a single posthog properties bag. // Applied to event.properties and to the person-profile bags ($set / $set_once), // since posthog mirrors the same URL fields into those. -function stripPrivacyFields( +function stripSensitiveFields( obj: Properties | undefined, anonymity: Anonymity, ): void { @@ -77,13 +77,13 @@ function stripPrivacyFields( if (anonymity === Anonymity.Anonymous) { // drop referrer information for anonymous users - obj["$referrer"] = null; - obj["$referring_domain"] = null; - obj["$initial_referrer"] = null; - obj["$initial_referring_domain"] = null; + delete obj["$referrer"]; + delete obj["$referring_domain"]; + delete obj["$initial_referrer"]; + delete obj["$initial_referring_domain"]; // drop device ID, which is a UUID persisted in local storage - obj["$device_id"] = null; + delete obj["$device_id"]; } // the url leaks a lot of private data like the call name or the user @@ -91,7 +91,12 @@ function stripPrivacyFields( // scheme + host so we still get host-level insights (develop / main / sfu). for (const key of ["$current_url", "$initial_current_url"]) { if (typeof obj[key] === "string") { - obj[key] = (obj[key] as string).split("/").slice(0, 3).join(""); + try { + const url = new URL(obj[key]); + obj[key] = url.protocol + "//" + url.hostname + url.pathname; + } catch { + obj[key] = null; + } } } @@ -116,14 +121,14 @@ export function applyPrivacyFilters( ): CaptureResult | null { if (event === null) return null; - stripPrivacyFields(event.properties, anonymity); + stripSensitiveFields(event.properties, anonymity); // posthog can stash person-profile updates either at the top level // of CaptureResult or nested inside properties depending on the pipeline // stage; clean both spots so nothing slips through. - stripPrivacyFields(event.$set, anonymity); - stripPrivacyFields(event.$set_once, anonymity); - stripPrivacyFields(event.properties["$set"], anonymity); - stripPrivacyFields(event.properties["$set_once"], anonymity); + stripSensitiveFields(event.$set, anonymity); + stripSensitiveFields(event.$set_once, anonymity); + stripSensitiveFields(event.properties["$set"], anonymity); + stripSensitiveFields(event.properties["$set_once"], anonymity); return event; } From bade32bf425d969fcebc1c3a1dff15e724084c2f Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 19 May 2026 17:40:30 +0200 Subject: [PATCH 6/6] update tests to check for `delete` (not anymore `=null`) rename: `applyPrivacyFilters`->`santizeSensitiveData` --- src/analytics/PosthogAnalytics.test.ts | 30 +++++++++++++------------- src/analytics/PosthogAnalytics.ts | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/analytics/PosthogAnalytics.test.ts b/src/analytics/PosthogAnalytics.test.ts index b5052840ca..7c1128ad4e 100644 --- a/src/analytics/PosthogAnalytics.test.ts +++ b/src/analytics/PosthogAnalytics.test.ts @@ -18,7 +18,7 @@ import posthog, { type CaptureResult } from "posthog-js"; import { Anonymity, - applyPrivacyFilters, + santizeSensitiveData, PosthogAnalytics, } from "./PosthogAnalytics"; import { mockConfig } from "../utils/test"; @@ -99,7 +99,7 @@ describe("PosthogAnalytics", () => { ({ event: "anyEvent", properties }) as unknown as CaptureResult; it("drops $initial_person_info regardless of anonymity", () => { - const out = applyPrivacyFilters( + const out = santizeSensitiveData( makeEvent({ $current_url: "https://call.example.com/some/private/path", $initial_person_info: { @@ -112,16 +112,16 @@ describe("PosthogAnalytics", () => { expect(out?.properties).not.toHaveProperty("$initial_person_info"); }); - it("strips path from $current_url", () => { - const out = applyPrivacyFilters( - makeEvent({ $current_url: "https://call.example.com/x/y/z" }), + it("strips hash from $current_url", () => { + const out = santizeSensitiveData( + makeEvent({ $current_url: "https://call.example.com/#/x/y/z" }), Anonymity.Pseudonymous, ); expect(out?.properties["$current_url"]).not.toContain("/x/y/z"); }); it("nulls referrer and device fields when anonymous", () => { - const out = applyPrivacyFilters( + const out = santizeSensitiveData( makeEvent({ $current_url: "https://x/y", $referrer: "https://leaky", @@ -130,19 +130,19 @@ describe("PosthogAnalytics", () => { }), Anonymity.Anonymous, ); - expect(out?.properties["$referrer"]).toBeNull(); - expect(out?.properties["$initial_referrer"]).toBeNull(); - expect(out?.properties["$device_id"]).toBeNull(); + expect(out?.properties["$referrer"]).toBeUndefined(); + expect(out?.properties["$initial_referrer"]).toBeUndefined(); + expect(out?.properties["$device_id"]).toBeUndefined(); }); it("passes null events through unchanged", () => { - expect(applyPrivacyFilters(null, Anonymity.Pseudonymous)).toBeNull(); + expect(santizeSensitiveData(null, Anonymity.Pseudonymous)).toBeNull(); }); it("strips URL fields nested inside $set_once", () => { const secretUrl = "https://call.example.com/room/#/?password=hunter2&roomId=abc"; - const out = applyPrivacyFilters( + const out = santizeSensitiveData( makeEvent({ $current_url: "https://call.example.com/x", $set_once: { @@ -165,7 +165,7 @@ describe("PosthogAnalytics", () => { it("strips URL fields nested inside $set", () => { const secretUrl = "https://call.example.com/room/#/?password=hunter2&roomId=abc"; - const out = applyPrivacyFilters( + const out = santizeSensitiveData( makeEvent({ $current_url: "https://call.example.com/x", $set: { @@ -182,7 +182,7 @@ describe("PosthogAnalytics", () => { }); it("nulls referrer fields inside $set_once when anonymous", () => { - const out = applyPrivacyFilters( + const out = santizeSensitiveData( makeEvent({ $current_url: "https://x/y", $set_once: { @@ -194,8 +194,8 @@ describe("PosthogAnalytics", () => { ); const setOnce = out?.properties["$set_once"] as Record; - expect(setOnce["$initial_referrer"]).toBeNull(); - expect(setOnce["$initial_referring_domain"]).toBeNull(); + expect(setOnce["$initial_referrer"]).toBeUndefined(); + expect(setOnce["$initial_referring_domain"]).toBeUndefined(); }); }); diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts index 915a6f2d0b..01a146e0d2 100644 --- a/src/analytics/PosthogAnalytics.ts +++ b/src/analytics/PosthogAnalytics.ts @@ -115,7 +115,7 @@ function stripSensitiveFields( * See src/utils/event-utils.ts in posthog-js (getEventProperties, getPersonInfo) * for the list of properties posthog sets automatically. */ -export function applyPrivacyFilters( +export function santizeSensitiveData( event: CaptureResult | null, anonymity: Anonymity, ): CaptureResult | null { @@ -198,7 +198,7 @@ export class PosthogAnalytics { if (apiKey && apiHost) { const beforeSend = (event: CaptureResult | null): CaptureResult | null => - applyPrivacyFilters(event, this.anonymity); + santizeSensitiveData(event, this.anonymity); this.posthog.init(apiKey, { api_host: apiHost, autocapture: false,