From f0db134b6ea9c7fa2cc9c101d3f5d3acac699302 Mon Sep 17 00:00:00 2001 From: fkwp Date: Tue, 19 May 2026 18:06:59 +0200 Subject: [PATCH] Posthog: drop $initial_person_info from outgoing events (#3968) * Posthog: drop $initial_person_info from outgoing events * Posthog: migrate from sanitize_properties to before_send * strip URL fields from $set / $set_once * enable mask_personal_data_properties * review * update tests to check for `delete` (not anymore `=null`) rename: `applyPrivacyFilters`->`santizeSensitiveData` --------- Co-authored-by: Timo K --- package.json | 2 +- pnpm-lock.yaml | 296 +++++++++++++++++++++++-- src/analytics/PosthogAnalytics.test.ts | 157 ++++++++++++- src/analytics/PosthogAnalytics.ts | 101 ++++++--- 4 files changed, 512 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 5d3bdb3d..dc8d3fbb 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 a0c2a148..8a33d40f 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) @@ -1672,6 +1672,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==} @@ -1880,6 +1952,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==} @@ -2959,6 +3067,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==} @@ -3699,6 +3810,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==} @@ -3921,6 +4035,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==} @@ -5059,6 +5176,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 @@ -5603,8 +5723,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==} @@ -5644,6 +5764,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==} @@ -5666,6 +5790,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'} @@ -6689,8 +6816,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==} @@ -8353,6 +8480,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': @@ -8488,6 +8691,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': {} @@ -9461,6 +9692,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': {} @@ -9772,7 +10006,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: @@ -10358,6 +10592,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): @@ -10590,6 +10826,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 @@ -11960,6 +12200,8 @@ snapshots: loglevel@1.9.2: {} + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -12631,11 +12873,21 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - posthog-js@1.160.3: + 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: {} @@ -12668,6 +12920,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: @@ -12693,6 +12960,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: {} @@ -13803,9 +14072,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)) @@ -13828,6 +14097,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 @@ -13851,7 +14121,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 49af5eae..7c1128ad 100644 --- a/src/analytics/PosthogAnalytics.test.ts +++ b/src/analytics/PosthogAnalytics.test.ts @@ -14,8 +14,13 @@ import { beforeAll, afterAll, } from "vitest"; +import posthog, { type CaptureResult } from "posthog-js"; -import { PosthogAnalytics } from "./PosthogAnalytics"; +import { + Anonymity, + santizeSensitiveData, + PosthogAnalytics, +} from "./PosthogAnalytics"; import { mockConfig } from "../utils/test"; describe("PosthogAnalytics", () => { @@ -88,4 +93,154 @@ describe("PosthogAnalytics", () => { expect(PosthogAnalytics.instance.isEnabled()).toBe(true); }); }); + + describe("applyPrivacyFilters", () => { + const makeEvent = (properties: Record): CaptureResult => + ({ event: "anyEvent", properties }) as unknown as CaptureResult; + + it("drops $initial_person_info regardless of anonymity", () => { + const out = santizeSensitiveData( + 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 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 = santizeSensitiveData( + makeEvent({ + $current_url: "https://x/y", + $referrer: "https://leaky", + $initial_referrer: "https://leaky-too", + $device_id: "uuid", + }), + Anonymity.Anonymous, + ); + expect(out?.properties["$referrer"]).toBeUndefined(); + expect(out?.properties["$initial_referrer"]).toBeUndefined(); + expect(out?.properties["$device_id"]).toBeUndefined(); + }); + + it("passes null events through unchanged", () => { + 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 = santizeSensitiveData( + 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 = santizeSensitiveData( + 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 = santizeSensitiveData( + 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"]).toBeUndefined(); + expect(setOnce["$initial_referring_domain"]).toBeUndefined(); + }); + }); + + // 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"); + }); + + beforeEach(() => { + mockConfig({ + posthog: { + api_host: "https://api.example.com.localhost", + api_key: "api_key", + }, + }); + PosthogAnalytics.resetInstance(); + }); + + afterAll(() => { + vi.unstubAllEnvs(); + }); + + it("passes events through the privacy filter via before_send", () => { + const initSpy = vi.spyOn(posthog, "init"); + expect(PosthogAnalytics.instance.isEnabled()).toBe(true); + + const beforeSend = initSpy.mock.calls[0][1]?.before_send; + expect(beforeSend).toBeInstanceOf(Function); + + const event = { + event: "anyEvent", + properties: { + $current_url: "https://call.example.com/x/y", + $initial_person_info: { r: "x" }, + }, + } as unknown as CaptureResult; + + 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 6ec8f8c7..01a146e0 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,73 @@ 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 stripSensitiveFields( + obj: Properties | undefined, + anonymity: Anonymity, +): void { + if (!obj) return; + + if (anonymity === Anonymity.Anonymous) { + // drop referrer information for anonymous users + 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 + delete obj["$device_id"]; + } + + // 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") { + try { + const url = new URL(obj[key]); + obj[key] = url.protocol + "//" + url.hostname + url.pathname; + } catch { + obj[key] = null; + } + } + } + + // $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, $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. + */ +export function santizeSensitiveData( + event: CaptureResult | null, + anonymity: Anonymity, +): CaptureResult | null { + if (event === null) return null; + + 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. + stripSensitiveFields(event.$set, anonymity); + stripSensitiveFields(event.$set_once, anonymity); + stripSensitiveFields(event.properties["$set"], anonymity); + stripSensitiveFields(event.properties["$set_once"], anonymity); + + return event; +} + interface PlatformProperties { appVersion: string; matrixBackend: "embedded" | "jssdk"; @@ -129,13 +197,16 @@ export class PosthogAnalytics { } if (apiKey && apiHost) { + const beforeSend = (event: CaptureResult | null): CaptureResult | null => + santizeSensitiveData(event, this.anonymity); this.posthog.init(apiKey, { api_host: apiHost, autocapture: false, mask_all_text: true, mask_all_element_attributes: true, + mask_personal_data_properties: true, capture_pageview: false, - sanitize_properties: this.sanitizeProperties, + before_send: beforeSend, respect_dnt: true, advanced_disable_decide: true, }); @@ -148,34 +219,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(""); - - return properties; - }; - private registerSuperProperties(properties: Properties): void { if (this.enabled) { this.posthog.register(properties);