diff --git a/.github/workflows/library-ci.yml b/.github/workflows/library-ci.yml index ca6dbcc6e..e7b8e8fe0 100644 --- a/.github/workflows/library-ci.yml +++ b/.github/workflows/library-ci.yml @@ -20,6 +20,7 @@ jobs: node-version: '18' cache: 'pnpm' - run: pnpm install + - run: pnpm build - run: pnpm test:unit integration: diff --git a/package.json b/package.json index 0137fa4dc..6caa2040b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build-rollup": "rm -rf lib && tsc -b && rollup -c --bundleConfigAsCjs", "build-react": "cd react; NODE_ENV=dev pnpm i; pnpm build;", "lint": "eslint src && eslint cypress", + "lint:fix": "eslint src --fix && eslint cypress --fix", "prettier": "prettier --write src/ functional_tests/", "prepublishOnly": "pnpm lint && pnpm test && pnpm build && pnpm test:react", "test": "pnpm test:unit && pnpm test:custom-eslint-rules && pnpm test:functional", @@ -100,14 +101,15 @@ "prettier": "^2.7.1", "rollup": "^4.9.6", "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-ts": "^3.4.5", "rollup-plugin-visualizer": "^5.12.0", "rrweb": "2.0.0-alpha.13", "rrweb-snapshot": "2.0.0-alpha.13", "sinon": "9.0.2", "testcafe": "1.19.0", "testcafe-browser-provider-browserstack": "1.14.0", - "tslib": "^2.5.0", "ts-node": "^10.9.2", + "tslib": "^2.5.0", "typescript": "^5.5.4", "yargs": "^17.7.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37b87f8da..1cd7708c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -195,6 +195,9 @@ devDependencies: rollup-plugin-dts: specifier: ^6.1.0 version: 6.1.0(rollup@4.9.6)(typescript@5.5.4) + rollup-plugin-ts: + specifier: ^3.4.5 + version: 3.4.5(@babel/core@7.18.9)(@babel/preset-env@7.18.9)(@babel/preset-typescript@7.18.6)(rollup@4.9.6)(typescript@5.5.4) rollup-plugin-visualizer: specifier: ^5.12.0 version: 5.12.0(rollup@4.9.6) @@ -2455,6 +2458,10 @@ packages: resolution: {integrity: sha512-jzPrheqEtrnUWWNUS8SFVbQAnoO6rOXetkjiJyzP92UM+BNcyExLD0Qikv9z6TU9D6A9rbXkh3pSmZ5G88d6ew==} dev: true + /@mdn/browser-compat-data@5.5.51: + resolution: {integrity: sha512-17L3+/oqX+sgSyucNKSipri1LkI/d8pwPQI4Vv2ejRVZLZr1WGxcEGBnglqFhdlislQBceJiHAdQnWEE+YJE3A==} + dev: true + /@miherlosev/esm@3.2.26: resolution: {integrity: sha512-TaW4jTGVE1/ln2VGFChnheMh589QCAZy1MVnLvjjSzZ4pEAa4WYAWPwFkDVZbSdPQdLfZy7LuTyZjWRkhX9/Gg==} engines: {node: '>=6'} @@ -3002,12 +3009,20 @@ packages: resolution: {integrity: sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA==} dev: true + /@types/node@17.0.45: + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + dev: true + /@types/node@22.5.0: resolution: {integrity: sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==} dependencies: undici-types: 6.19.8 dev: true + /@types/object-path@0.11.4: + resolution: {integrity: sha512-4tgJ1Z3elF/tOMpA8JLVuR9spt9Ynsf7+JjqsQ2IqtiPJtcLoHoXcT6qU4E10cPFqyXX5HDm9QwIzZhBSkLxsw==} + dev: true + /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: true @@ -3046,6 +3061,10 @@ packages: resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} dev: true + /@types/semver@7.5.8: + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + dev: true + /@types/set-cookie-parser@2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: @@ -3074,6 +3093,10 @@ packages: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true + /@types/ua-parser-js@0.7.39: + resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} + dev: true + /@types/uuid@9.0.1: resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} dev: true @@ -3304,6 +3327,11 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true + /@wessberg/stringutil@1.0.19: + resolution: {integrity: sha512-9AZHVXWlpN8Cn9k5BC/O0Dzb9E9xfEMXzYrNunwvkUTvuK7xgQPVRZpLo+jWCOZ5r8oBa8NIrHuPEu1hzbb6bg==} + engines: {node: '>=8.0.0'} + dev: true + /@xmldom/xmldom@0.8.7: resolution: {integrity: sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==} engines: {node: '>=10.0.0'} @@ -3418,6 +3446,11 @@ packages: engines: {node: '>=6'} dev: true + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -4009,6 +4042,22 @@ packages: resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} dev: true + /browserslist-generator@2.3.0: + resolution: {integrity: sha512-NEvS2dNlBKfSL3qDUTM3NkJMfjMAPEjvEGnhMZKql6ZNzJ8asqFpmuTizwOpRQeYA0/VktmOXa+mFPv8nvcIGw==} + engines: {node: '>=16.15.1', npm: '>=7.0.0', pnpm: '>=3.2.0', yarn: '>=1.13'} + dependencies: + '@mdn/browser-compat-data': 5.5.51 + '@types/object-path': 0.11.4 + '@types/semver': 7.5.8 + '@types/ua-parser-js': 0.7.39 + browserslist: 4.23.3 + caniuse-lite: 1.0.30001660 + isbot: 3.8.0 + object-path: 0.11.8 + semver: 7.6.3 + ua-parser-js: 1.0.38 + dev: true + /browserslist@4.21.7: resolution: {integrity: sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4020,6 +4069,17 @@ packages: update-browserslist-db: 1.0.11(browserslist@4.21.7) dev: true + /browserslist@4.23.3: + resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001660 + electron-to-chromium: 1.5.19 + node-releases: 2.0.18 + update-browserslist-db: 1.1.0(browserslist@4.23.3) + dev: true + /browserstack-local@1.5.1: resolution: {integrity: sha512-T/wxyWDzvBHbDvl7fZKpFU7mYze6nrUkBhNy+d+8bXBqgQX10HTYvajIGO0wb49oGSLCPM0CMZTV/s7e6LF0sA==} dependencies: @@ -4135,6 +4195,10 @@ packages: resolution: {integrity: sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==} dev: true + /caniuse-lite@1.0.30001660: + resolution: {integrity: sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==} + dev: true + /capture-exit@2.0.0: resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} engines: {node: 6.* || 8.* || >= 10.*} @@ -4407,6 +4471,16 @@ packages: resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==} dev: true + /compatfactory@3.0.0(typescript@5.5.4): + resolution: {integrity: sha512-WD5kF7koPwVoyKL8p0LlrmIZtilrD46sQStyzzxzTFinMKN2Dxk1hN+sddLSQU1mGIZvQfU8c+ONSghvvM40jg==} + engines: {node: '>=14.9.0'} + peerDependencies: + typescript: '>=3.x || >= 4.x || >= 5.x' + dependencies: + helpertypes: 0.0.19 + typescript: 5.5.4 + dev: true + /component-emitter@1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} dev: true @@ -4503,6 +4577,13 @@ packages: which: 2.0.2 dev: true + /crosspath@2.0.0: + resolution: {integrity: sha512-ju88BYCQ2uvjO2bR+SsgLSTwTSctU+6Vp2ePbKPgSCZyy4MWZxYsT738DlKVRE5utUjobjPRm1MkTYKJxCmpTA==} + engines: {node: '>=14.9.0'} + dependencies: + '@types/node': 17.0.45 + dev: true + /crypto-md5@1.0.0: resolution: {integrity: sha512-65Mtei8+EkSIK+5Ie4gpWXoJ/5bgpqPXFknHHXAyhDqKsEAAzUslGd8mOeawbfcuQ8fADNKcF4xQA3fqlZJ8Ig==} engines: {iojs: '>=1.0.0', node: '>=0.5.2'} @@ -4913,6 +4994,10 @@ packages: resolution: {integrity: sha512-1KnpDTS9onwAfMzW50LcpNtyOkMyjd/OLoD2Kx/DDITZqgNYixY71XNszPHNxyQQ/Brh+FDcUnf4BaM041sdWg==} dev: true + /electron-to-chromium@1.5.19: + resolution: {integrity: sha512-kpLJJi3zxTR1U828P+LIUDZ5ohixyo68/IcYOHLqnbTPr/wdgn4i1ECvmALN9E16JPA6cvCG5UG79gVwVdEK5w==} + dev: true + /elegant-spinner@1.0.1: resolution: {integrity: sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==} engines: {node: '>=0.10.0'} @@ -5041,6 +5126,11 @@ packages: engines: {node: '>=6'} dev: true + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + dev: true + /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} dev: true @@ -6116,6 +6206,11 @@ packages: resolution: {integrity: sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==} dev: true + /helpertypes@0.0.19: + resolution: {integrity: sha512-J00e55zffgi3yVnUp0UdbMztNkr2PnizEkOe9URNohnrNhW5X0QpegkuLpOmFQInpi93Nb8MCjQRHAiCDF42NQ==} + engines: {node: '>=10.0.0'} + dev: true + /highlight-es@1.0.3: resolution: {integrity: sha512-s/SIX6yp/5S1p8aC/NRDC1fwEb+myGIfp8/TzZz0rtAv8fzsdX7vGl3Q1TrXCsczFq8DI3CBFBCySPClfBSdbg==} dependencies: @@ -6788,6 +6883,11 @@ packages: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: true + /isbot@3.8.0: + resolution: {integrity: sha512-vne1mzQUTR+qsMLeCBL9+/tgnDXRyc2pygLGl/WsgA+EZKIiB5Ehu0CiVTHIIk30zhJ24uGz4M5Ppse37aR0Hg==} + engines: {node: '>=12'} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -8298,6 +8398,10 @@ packages: resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} dev: true + /node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + dev: true + /normalize-path@2.1.1: resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} engines: {node: '>=0.10.0'} @@ -8363,6 +8467,11 @@ packages: engines: {node: '>= 0.4'} dev: true + /object-path@0.11.8: + resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} + engines: {node: '>= 10.12.0'} + dev: true + /object-visit@1.0.1: resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} engines: {node: '>=0.10.0'} @@ -8726,6 +8835,10 @@ packages: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true + /picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + dev: true + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -9345,6 +9458,52 @@ packages: '@babel/code-frame': 7.23.5 dev: true + /rollup-plugin-ts@3.4.5(@babel/core@7.18.9)(@babel/preset-env@7.18.9)(@babel/preset-typescript@7.18.6)(rollup@4.9.6)(typescript@5.5.4): + resolution: {integrity: sha512-9iCstRJpEZXSRQuXitlSZAzcGlrqTbJg1pE4CMbEi6xYldxVncdPyzA2I+j6vnh73wBymZckerS+Q/iEE/M3Ow==} + engines: {node: '>=16.15.1', npm: '>=7.0.0', pnpm: '>=3.2.0', yarn: '>=1.13'} + peerDependencies: + '@babel/core': '>=7.x' + '@babel/plugin-transform-runtime': '>=7.x' + '@babel/preset-env': '>=7.x' + '@babel/preset-typescript': '>=7.x' + '@babel/runtime': '>=7.x' + '@swc/core': '>=1.x' + '@swc/helpers': '>=0.2' + rollup: '>=1.x || >=2.x || >=3.x' + typescript: '>=3.2.x || >= 4.x || >= 5.x' + peerDependenciesMeta: + '@babel/core': + optional: true + '@babel/plugin-transform-runtime': + optional: true + '@babel/preset-env': + optional: true + '@babel/preset-typescript': + optional: true + '@babel/runtime': + optional: true + '@swc/core': + optional: true + '@swc/helpers': + optional: true + dependencies: + '@babel/core': 7.18.9 + '@babel/preset-env': 7.18.9(@babel/core@7.18.9) + '@babel/preset-typescript': 7.18.6(@babel/core@7.18.9) + '@rollup/pluginutils': 5.1.0(rollup@4.9.6) + '@wessberg/stringutil': 1.0.19 + ansi-colors: 4.1.3 + browserslist: 4.23.3 + browserslist-generator: 2.3.0 + compatfactory: 3.0.0(typescript@5.5.4) + crosspath: 2.0.0 + magic-string: 0.30.5 + rollup: 4.9.6 + ts-clone-node: 3.0.0(typescript@5.5.4) + tslib: 2.7.0 + typescript: 5.5.4 + dev: true + /rollup-plugin-visualizer@5.12.0(rollup@4.9.6): resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} engines: {node: '>=14'} @@ -9435,7 +9594,7 @@ packages: /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - tslib: 2.5.0 + tslib: 2.7.0 dev: true /safe-buffer@5.1.2: @@ -10450,6 +10609,16 @@ packages: typescript: 5.5.4 dev: true + /ts-clone-node@3.0.0(typescript@5.5.4): + resolution: {integrity: sha512-egavvyHbIoelkgh1IC2agNB1uMNjB8VJgh0g/cn0bg2XXTcrtjrGMzEk4OD3Fi2hocICjP3vMa56nkzIzq0FRg==} + engines: {node: '>=14.9.0'} + peerDependencies: + typescript: ^3.x || ^4.x || ^5.x + dependencies: + compatfactory: 3.0.0(typescript@5.5.4) + typescript: 5.5.4 + dev: true + /ts-node@10.9.2(@types/node@22.5.0)(typescript@5.5.4): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -10489,6 +10658,10 @@ packages: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} dev: true + /tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + dev: true + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -10559,6 +10732,10 @@ packages: hasBin: true dev: true + /ua-parser-js@1.0.38: + resolution: {integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==} + dev: true + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -10658,6 +10835,17 @@ packages: picocolors: 1.0.0 dev: true + /update-browserslist-db@1.1.0(browserslist@4.23.3): + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.3 + escalade: 3.2.0 + picocolors: 1.1.0 + dev: true + /uri-js@4.2.2: resolution: {integrity: sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==} dependencies: diff --git a/rollup.config.js b/rollup.config.js index e2ca5932e..3bd5afe8c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,10 +3,10 @@ import json from '@rollup/plugin-json' import resolve from '@rollup/plugin-node-resolve' import typescript from '@rollup/plugin-typescript' import { dts } from 'rollup-plugin-dts' -import pkg from './package.json' import terser from '@rollup/plugin-terser' import { visualizer } from 'rollup-plugin-visualizer' import fs from 'fs' +import path from 'path' const plugins = [ json(), @@ -20,9 +20,9 @@ const plugins = [ terser({ toplevel: true }), ] -/** @type {import('rollup').RollupOptions[]} */ +const entrypoints = fs.readdirSync('./src/entrypoints') -const entrypoints = fs.readdirSync('./src/entrypoints').map((file) => { +const entrypointTargets = entrypoints.map((file) => { const fileParts = file.split('.') // pop the extension fileParts.pop() @@ -40,6 +40,8 @@ const entrypoints = fs.readdirSync('./src/entrypoints').map((file) => { // we're allowed to console log in this file :) // eslint-disable-next-line no-console console.log(`Building ${fileName} in ${format} format`) + + /** @type {import('rollup').RollupOptions} */ return { input: `src/entrypoints/${file}`, output: [ @@ -62,15 +64,26 @@ const entrypoints = fs.readdirSync('./src/entrypoints').map((file) => { } }) -export default [ - ...entrypoints, - { - input: './lib/src/entrypoints/module.es.d.ts', - output: [{ file: pkg.types, format: 'es' }], - plugins: [ - dts({ - respectExternal: true, - }), - ], - }, -] +const typeTargets = entrypoints + .filter((file) => file.endsWith('.es.ts')) + .map((file) => { + const source = `./lib/src/entrypoints/${file.replace('.ts', '.d.ts')}` + /** @type {import('rollup').RollupOptions} */ + return { + input: source, + output: [ + { + dir: path.resolve('./dist'), + entryFileNames: file.replace('.es.ts', '.d.ts'), + }, + ], + plugins: [ + json(), + dts({ + exclude: [], + }), + ], + } + }) + +export default [...entrypointTargets, ...typeTargets] diff --git a/src/__tests__/decide.ts b/src/__tests__/decide.ts index 37225a1a4..534d64c65 100644 --- a/src/__tests__/decide.ts +++ b/src/__tests__/decide.ts @@ -4,6 +4,7 @@ import { RequestRouter } from '../utils/request-router' import { expectScriptToExist, expectScriptToNotExist } from './helpers/script-utils' import { PostHog } from '../posthog-core' import { DecideResponse, PostHogConfig, Properties } from '../types' +import '../entrypoints/external-scripts-loader' const expectDecodedSendRequest = ( send_request: PostHog['_send_request'], diff --git a/src/__tests__/entrypoints/module.test.ts b/src/__tests__/entrypoints/module.test.ts new file mode 100644 index 000000000..f0214b34e --- /dev/null +++ b/src/__tests__/entrypoints/module.test.ts @@ -0,0 +1,38 @@ +import fs from 'fs' +import path from 'path' +// Sanity checks to check the built code does not contain any script loaders + +describe('Array entrypoint', () => { + const arrayJs = fs.readFileSync(path.join(__dirname, '../../../dist/array.js'), 'utf-8') + const arrayFullJs = fs.readFileSync(path.join(__dirname, '../../../dist/array.full.js'), 'utf-8') + const arrayNoExternalJs = fs.readFileSync(path.join(__dirname, '../../../dist/array.no-external.js'), 'utf-8') + const arrayFullNoExternalJs = fs.readFileSync( + path.join(__dirname, '../../../dist/array.full.no-external.js'), + 'utf-8' + ) + + it('should not contain any script loaders', () => { + expect(arrayJs).toContain('loadExternalDependency=') + expect(arrayFullJs).toContain('loadExternalDependency=') + expect(arrayNoExternalJs).not.toContain('loadExternalDependency=') + expect(arrayFullNoExternalJs).not.toContain('loadExternalDependency=') + }) +}) + +describe('Module entrypoint', () => { + const moduleJs = fs.readFileSync(path.join(__dirname, '../../../dist/module.js'), 'utf-8') + const moduleFullJs = fs.readFileSync(path.join(__dirname, '../../../dist/module.full.js'), 'utf-8') + const moduleNoExternalJs = fs.readFileSync(path.join(__dirname, '../../../dist/module.no-external.js'), 'utf-8') + const moduleFullNoExternalJs = fs.readFileSync( + path.join(__dirname, '../../../dist/module.full.no-external.js'), + 'utf-8' + ) + + it('should not contain any script loaders', () => { + // For the module loader, the code isn't minified + expect(moduleJs).toContain('loadExternalDependency=') + expect(moduleFullJs).toContain('loadExternalDependency=') + expect(moduleNoExternalJs).not.toContain('loadExternalDependency=') + expect(moduleFullNoExternalJs).not.toContain('loadExternalDependency=') + }) +}) diff --git a/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts index 2070a2c41..42acf7292 100644 --- a/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts +++ b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts @@ -46,13 +46,15 @@ describe('Exception Observer', () => { } beforeEach(async () => { - loadScriptMock.mockImplementation((_path, callback) => { + loadScriptMock.mockImplementation((_ph, _path, callback) => { addErrorWrappingFlagToWindow() callback() }) posthog = await createPosthogInstance(uuidv7(), { _onCapture: mockCapture }) - posthog.requestRouter.loadScript = loadScriptMock + assignableWindow.__PosthogExtensions__ = { + loadExternalDependency: loadScriptMock, + } sendRequestSpy = jest.spyOn(posthog, '_send_request') diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts index fd26a40c6..7bdf8effd 100644 --- a/src/__tests__/extensions/replay/sessionrecording.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording.test.ts @@ -203,12 +203,12 @@ describe('SessionRecording', () => { consent: { isOptedOut: () => false }, } as unknown as PostHog - loadScriptMock.mockImplementation((_path, callback) => { + loadScriptMock.mockImplementation((_ph, _path, callback) => { addRRwebToWindow() callback() }) - posthog.requestRouter.loadScript = loadScriptMock + assignableWindow.__PosthogExtensions__.loadExternalDependency = loadScriptMock // defaults posthog.persistence?.register({ @@ -908,7 +908,7 @@ describe('SessionRecording', () => { it('loads recording script from right place', () => { sessionRecording.startIfEnabledOrStop() - expect(loadScriptMock).toHaveBeenCalledWith('/static/recorder.js?v=v0.0.1', expect.anything()) + expect(loadScriptMock).toHaveBeenCalledWith(expect.anything(), 'recorder', expect.anything()) }) it('loads script after `_startCapture` if not previously loaded', () => { @@ -1825,7 +1825,7 @@ describe('SessionRecording', () => { describe('when rrweb is not available', () => { beforeEach(() => { // Fake rrweb not being available - loadScriptMock.mockImplementation((_path, callback) => { + loadScriptMock.mockImplementation((_ph, _path, callback) => { callback() }) diff --git a/src/__tests__/extensions/toolbar.test.ts b/src/__tests__/extensions/toolbar.test.ts index d6d0ec0bd..c1a32aa1b 100644 --- a/src/__tests__/extensions/toolbar.test.ts +++ b/src/__tests__/extensions/toolbar.test.ts @@ -27,7 +27,10 @@ describe('Toolbar', () => { set_config: jest.fn(), } as unknown as PostHog - instance.requestRouter.loadScript = jest.fn((_path: any, callback: any) => callback()) + assignableWindow.__PosthogExtensions__ = { + loadExternalDependency: jest.fn((_ph, _path: any, callback: any) => callback()), + } + toolbar = new Toolbar(instance) }) diff --git a/src/__tests__/extensions/web-vitals.test.ts b/src/__tests__/extensions/web-vitals.test.ts index 37233f0c5..80c8738d6 100644 --- a/src/__tests__/extensions/web-vitals.test.ts +++ b/src/__tests__/extensions/web-vitals.test.ts @@ -89,7 +89,7 @@ describe('web vitals', () => { capture_pageview: false, }) - loadScriptMock.mockImplementation((_path, callback) => { + loadScriptMock.mockImplementation((_ph, _path, callback) => { // we need a set of fake web vitals handlers, so we can manually trigger the events assignableWindow.__PosthogExtensions__ = {} assignableWindow.__PosthogExtensions__.postHogWebVitalsCallbacks = { @@ -109,7 +109,8 @@ describe('web vitals', () => { callback() }) - posthog.requestRouter.loadScript = loadScriptMock + assignableWindow.__PosthogExtensions__ = {} + assignableWindow.__PosthogExtensions__.loadExternalDependency = loadScriptMock // need to force this to get the web vitals script loaded posthog.webVitalsAutocapture!.afterDecideResponse({ diff --git a/src/__tests__/surveys.test.ts b/src/__tests__/surveys.test.ts index 0074a3bf0..868df4860 100644 --- a/src/__tests__/surveys.test.ts +++ b/src/__tests__/surveys.test.ts @@ -127,7 +127,7 @@ describe('surveys', () => { const loadScriptMock = jest.fn() - loadScriptMock.mockImplementation((_path, callback) => { + loadScriptMock.mockImplementation((_ph, _path, callback) => { assignableWindow.__PosthogExtensions__ = assignableWindow.__Posthog__ || {} assignableWindow.__PosthogExtensions__.generateSurveys = generateSurveys assignableWindow.__PosthogExtensions__.canActivateRepeatedly = canActivateRepeatedly @@ -163,7 +163,9 @@ describe('surveys', () => { }, } as unknown as PostHog - instance.requestRouter.loadScript = loadScriptMock + assignableWindow.__PosthogExtensions__ = { + loadExternalDependency: loadScriptMock, + } surveys = new PostHogSurveys(instance) instance.surveys = surveys @@ -177,8 +179,6 @@ describe('surveys', () => { // eslint-disable-next-line compat/compat value: new URL('https://example.com'), }) - - surveys.afterDecideResponse(decideResponse) }) afterEach(() => { diff --git a/src/__tests__/utils/external-scripts-loader.test.ts b/src/__tests__/utils/external-scripts-loader.test.ts new file mode 100644 index 000000000..b85791d00 --- /dev/null +++ b/src/__tests__/utils/external-scripts-loader.test.ts @@ -0,0 +1,67 @@ +import { RequestRouter } from '../../utils/request-router' +import { assignableWindow } from '../../utils/globals' +import { PostHog } from '../../posthog-core' +import '../../entrypoints/external-scripts-loader' + +describe('external-scripts-loader', () => { + describe('loadScript', () => { + const mockPostHog: PostHog = { + config: { + api_host: 'https://us.posthog.com', + }, + version: '1.0.0', + } as PostHog + mockPostHog.requestRouter = new RequestRouter(mockPostHog) + + const callback = jest.fn() + beforeEach(() => { + callback.mockClear() + document!.getElementsByTagName('html')![0].innerHTML = '' + }) + + it('should insert the given script before the one already on the page', () => { + document!.body.appendChild(document!.createElement('script')) + assignableWindow.__PosthogExtensions__.loadExternalDependency(mockPostHog, 'recorder', callback) + const scripts = document!.getElementsByTagName('script') + const new_script = scripts[0] + + expect(scripts.length).toBe(2) + expect(new_script.type).toBe('text/javascript') + expect(new_script.src).toMatchInlineSnapshot(`"https://us-assets.i.posthog.com/static/recorder.js?v=1.0.0"`) + const event = new Event('test') + new_script.onload!(event) + expect(callback).toHaveBeenCalledWith(undefined, event) + }) + + it("should add the script to the page when there aren't any preexisting scripts on the page", () => { + assignableWindow.__PosthogExtensions__.loadExternalDependency(mockPostHog, 'recorder', callback) + const scripts = document!.getElementsByTagName('script') + + expect(scripts?.length).toBe(1) + expect(scripts![0].type).toBe('text/javascript') + expect(scripts![0].src).toMatchInlineSnapshot( + `"https://us-assets.i.posthog.com/static/recorder.js?v=1.0.0"` + ) + }) + + it('should respond with an error if one happens', () => { + assignableWindow.__PosthogExtensions__.loadExternalDependency(mockPostHog, 'recorder', callback) + const scripts = document!.getElementsByTagName('script') + const new_script = scripts[0] + + new_script.onerror!('uh-oh') + expect(callback).toHaveBeenCalledWith('uh-oh') + }) + + it('should add a timestamp to the toolbar loader', () => { + jest.useFakeTimers() + jest.setSystemTime(1726067100000) + assignableWindow.__PosthogExtensions__.loadExternalDependency(mockPostHog, 'toolbar', callback) + const scripts = document!.getElementsByTagName('script') + const new_script = scripts[0] + expect(new_script.src).toMatchInlineSnapshot( + `"https://us-assets.i.posthog.com/static/toolbar.js?v=1.0.0?&=1726067100000"` + ) + }) + }) +}) diff --git a/src/__tests__/utils/request-router.test.ts b/src/__tests__/utils/request-router.test.ts index 9075669d9..3715cf90e 100644 --- a/src/__tests__/utils/request-router.test.ts +++ b/src/__tests__/utils/request-router.test.ts @@ -80,53 +80,4 @@ describe('request-router', () => { mockPostHog.config.api_host = 'https://eu.posthog.com' expect(router.endpointFor('api')).toEqual('https://eu.i.posthog.com') }) - - describe('loadScript', () => { - const theRouter = router() - const callback = jest.fn() - beforeEach(() => { - callback.mockClear() - document!.getElementsByTagName('html')![0].innerHTML = '' - }) - - it('should insert the given script before the one already on the page', () => { - document!.body.appendChild(document!.createElement('script')) - theRouter.loadScript('https://fake_url', callback) - const scripts = document!.getElementsByTagName('script') - const new_script = scripts[0] - - expect(scripts.length).toBe(2) - expect(new_script.type).toBe('text/javascript') - expect(new_script.src).toBe('https://fake_url/') - const event = new Event('test') - new_script.onload!(event) - expect(callback).toHaveBeenCalledWith(undefined, event) - }) - - it("should add the script to the page when there aren't any preexisting scripts on the page", () => { - theRouter.loadScript('https://fake_url', callback) - const scripts = document!.getElementsByTagName('script') - - expect(scripts?.length).toBe(1) - expect(scripts![0].type).toBe('text/javascript') - expect(scripts![0].src).toBe('https://fake_url/') - }) - - it('should respond with an error if one happens', () => { - theRouter.loadScript('https://fake_url', callback) - const scripts = document!.getElementsByTagName('script') - const new_script = scripts[0] - - new_script.onerror!('uh-oh') - expect(callback).toHaveBeenCalledWith('uh-oh') - }) - - it('should prefix with assets url if not already prefixed', () => { - theRouter.loadScript('/static/recorder.js', callback) - const scripts = document!.getElementsByTagName('script') - const new_script = scripts[0] - expect(new_script.type).toBe('text/javascript') - expect(new_script.src).toBe('https://us-assets.i.posthog.com/static/recorder.js') - }) - }) }) diff --git a/src/decide.ts b/src/decide.ts index 2bc23b9a1..9ccb59819 100644 --- a/src/decide.ts +++ b/src/decide.ts @@ -68,11 +68,8 @@ export class Decide { if (response['siteApps']) { if (this.instance.config.opt_in_site_apps) { for (const { id, url } of response['siteApps']) { - const scriptUrl = this.instance.requestRouter.endpointFor('api', url) - assignableWindow[`__$$ph_site_app_${id}`] = this.instance - - this.instance.requestRouter.loadScript(scriptUrl, (err) => { + assignableWindow.__PosthogExtensions__?.loadSiteApp?.(this.instance, url, (err) => { if (err) { return logger.error(`Error while initializing PostHog app with config id ${id}`, err) } diff --git a/src/entrypoints/all-external-dependencies.ts b/src/entrypoints/all-external-dependencies.ts new file mode 100644 index 000000000..65a06bf92 --- /dev/null +++ b/src/entrypoints/all-external-dependencies.ts @@ -0,0 +1,5 @@ +import './recorder' +import './surveys' +import './exception-autocapture' +import './tracing-headers' +import './web-vitals' diff --git a/src/entrypoints/array.full.no-external.ts b/src/entrypoints/array.full.no-external.ts new file mode 100644 index 000000000..b5c1a1d11 --- /dev/null +++ b/src/entrypoints/array.full.no-external.ts @@ -0,0 +1,3 @@ +// Same as loader-globals.ts except includes all additional extension loaders +import './all-external-dependencies' +import './array.no-external' diff --git a/src/entrypoints/array.full.ts b/src/entrypoints/array.full.ts index 0ca198803..507e0e4f1 100644 --- a/src/entrypoints/array.full.ts +++ b/src/entrypoints/array.full.ts @@ -1,11 +1,3 @@ // Same as loader-globals.ts except includes all additional extension loaders - -import './recorder' -import './surveys' -import './exception-autocapture' -import './tracing-headers' -import './web-vitals' - -import { init_from_snippet } from '../posthog-core' - -init_from_snippet() +import './all-external-dependencies' +import './array' diff --git a/src/entrypoints/array.no-external.ts b/src/entrypoints/array.no-external.ts new file mode 100644 index 000000000..9a29281a7 --- /dev/null +++ b/src/entrypoints/array.no-external.ts @@ -0,0 +1,3 @@ +import { init_from_snippet } from '../posthog-core' + +init_from_snippet() diff --git a/src/entrypoints/array.ts b/src/entrypoints/array.ts index 9a29281a7..c2e1b2ae7 100644 --- a/src/entrypoints/array.ts +++ b/src/entrypoints/array.ts @@ -1,3 +1,2 @@ -import { init_from_snippet } from '../posthog-core' - -init_from_snippet() +import './external-scripts-loader' +import './array.no-external' diff --git a/src/entrypoints/external-scripts-loader.ts b/src/entrypoints/external-scripts-loader.ts new file mode 100644 index 000000000..7748292d2 --- /dev/null +++ b/src/entrypoints/external-scripts-loader.ts @@ -0,0 +1,67 @@ +import type { PostHog } from '../posthog-core' +import { assignableWindow, document, PostHogExtensionKind } from '../utils/globals' +import { logger } from '../utils/logger' + +const loadScript = (posthog: PostHog, url: string, callback: (error?: string | Event, event?: Event) => void) => { + if (posthog.config.disable_external_dependency_loading) { + logger.warn(`${url} was requested but loading of external scripts is disabled.`) + return callback('Loading of external scripts is disabled') + } + + const addScript = () => { + if (!document) { + return callback('document not found') + } + const scriptTag = document.createElement('script') + scriptTag.type = 'text/javascript' + scriptTag.src = url + scriptTag.onload = (event) => callback(undefined, event) + scriptTag.onerror = (error) => callback(error) + + const scripts = document.querySelectorAll('body > script') + if (scripts.length > 0) { + scripts[0].parentNode?.insertBefore(scriptTag, scripts[0]) + } else { + // In exceptional situations this call might load before the DOM is fully ready. + document.body.appendChild(scriptTag) + } + } + + if (document?.body) { + addScript() + } else { + document?.addEventListener('DOMContentLoaded', addScript) + } +} + +assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {} +assignableWindow.__PosthogExtensions__.loadExternalDependency = ( + posthog: PostHog, + kind: PostHogExtensionKind, + callback: (error?: string | Event, event?: Event) => void +): void => { + let scriptUrlToLoad = `/static/${kind}.js` + `?v=${posthog.version}` + + if (kind === 'toolbar') { + // toolbar.js is served from the PostHog CDN, this has a TTL of 24 hours. + // the toolbar asset includes a rotating "token" that is valid for 5 minutes. + const fiveMinutesInMillis = 5 * 60 * 1000 + // this ensures that we bust the cache periodically + const timestampToNearestFiveMinutes = Math.floor(Date.now() / fiveMinutesInMillis) * fiveMinutesInMillis + + scriptUrlToLoad = `${scriptUrlToLoad}?&=${timestampToNearestFiveMinutes}` + } + const url = posthog.requestRouter.endpointFor('assets', scriptUrlToLoad) + + loadScript(posthog, url, callback) +} + +assignableWindow.__PosthogExtensions__.loadSiteApp = ( + posthog: PostHog, + url: string, + callback: (error?: string | Event, event?: Event) => void +): void => { + const scriptUrl = posthog.requestRouter.endpointFor('api', url) + + loadScript(posthog, scriptUrl, callback) +} diff --git a/src/entrypoints/main.cjs.ts b/src/entrypoints/main.cjs.ts index 783b3dfd6..8068f3b16 100644 --- a/src/entrypoints/main.cjs.ts +++ b/src/entrypoints/main.cjs.ts @@ -1,3 +1,4 @@ +import './external-scripts-loader' import { init_as_module } from '../posthog-core' export { PostHog } from '../posthog-core' export * from '../types' diff --git a/src/entrypoints/module.es.ts b/src/entrypoints/module.es.ts index 783b3dfd6..2a8b5fe41 100644 --- a/src/entrypoints/module.es.ts +++ b/src/entrypoints/module.es.ts @@ -1,6 +1,4 @@ -import { init_as_module } from '../posthog-core' -export { PostHog } from '../posthog-core' -export * from '../types' -export * from '../posthog-surveys-types' -export const posthog = init_as_module() +import './external-scripts-loader' +import posthog from './module.no-external.es' +export * from './module.no-external.es' export default posthog diff --git a/src/entrypoints/module.full.es.ts b/src/entrypoints/module.full.es.ts new file mode 100644 index 000000000..fd8cf5262 --- /dev/null +++ b/src/entrypoints/module.full.es.ts @@ -0,0 +1,4 @@ +import './all-external-dependencies' +import posthog from './module.es' +export * from './module.es' +export default posthog diff --git a/src/entrypoints/module.full.no-external.es.ts b/src/entrypoints/module.full.no-external.es.ts new file mode 100644 index 000000000..5427bcd94 --- /dev/null +++ b/src/entrypoints/module.full.no-external.es.ts @@ -0,0 +1,4 @@ +import './all-external-dependencies' +import posthog from './module.no-external.es' +export * from './module.no-external.es' +export default posthog diff --git a/src/entrypoints/module.no-external.es.ts b/src/entrypoints/module.no-external.es.ts new file mode 100644 index 000000000..783b3dfd6 --- /dev/null +++ b/src/entrypoints/module.no-external.es.ts @@ -0,0 +1,6 @@ +import { init_as_module } from '../posthog-core' +export { PostHog } from '../posthog-core' +export * from '../types' +export * from '../posthog-surveys-types' +export const posthog = init_as_module() +export default posthog diff --git a/src/extensions/exception-autocapture/index.ts b/src/extensions/exception-autocapture/index.ts index be1996015..6f278dde9 100644 --- a/src/extensions/exception-autocapture/index.ts +++ b/src/extensions/exception-autocapture/index.ts @@ -4,7 +4,6 @@ import { DecideResponse, Properties } from '../../types' import { logger } from '../../utils/logger' import { EXCEPTION_CAPTURE_ENABLED_SERVER_SIDE } from '../../constants' -import Config from '../../config' const LOGGER_PREFIX = '[Exception Autocapture]' @@ -47,12 +46,16 @@ export class ExceptionObserver { cb() } - this.instance.requestRouter.loadScript(`/static/exception-autocapture.js?v=${Config.LIB_VERSION}`, (err) => { - if (err) { - return logger.error(LOGGER_PREFIX + ' failed to load script', err) + assignableWindow.__PosthogExtensions__?.loadExternalDependency?.( + this.instance, + 'exception-autocapture', + (err) => { + if (err) { + return logger.error(LOGGER_PREFIX + ' failed to load script', err) + } + cb() } - cb() - }) + ) } private startCapturing = () => { diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index 30a63dd4e..63d420f80 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -18,7 +18,6 @@ import { import { PostHog } from '../../posthog-core' import { DecideResponse, FlagVariant, NetworkRecordOptions, NetworkRequest, Properties } from '../../types' import { EventType, type eventWithTime, IncrementalSource, type listenerHandler, RecordPlugin } from '@rrweb/types' -import Config from '../../config' import { timestamp } from '../../utils' import { isBoolean, isFunction, isNullish, isNumber, isObject, isString, isUndefined } from '../../utils/type-utils' @@ -526,9 +525,9 @@ export class SessionRecording { // If recorder.js is already loaded (if array.full.js snippet is used or posthog-js/dist/recorder is // imported), don't load script. Otherwise, remotely import recorder.js from cdn since it hasn't been loaded. if (!this.rrwebRecord) { - this.instance.requestRouter.loadScript(`/static/recorder.js?v=${Config.LIB_VERSION}`, (err) => { + assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'recorder', (err) => { if (err) { - return logger.error(LOGGER_PREFIX + ` could not load recorder.js`, err) + return logger.error(LOGGER_PREFIX + ` could not load recorder`, err) } this._onScriptLoaded() diff --git a/src/extensions/toolbar.ts b/src/extensions/toolbar.ts index 27525fd7b..bf13d0ae7 100644 --- a/src/extensions/toolbar.ts +++ b/src/extensions/toolbar.ts @@ -158,13 +158,7 @@ export class Toolbar { // only load the toolbar once, even if there are multiple instances of PostHogLib this.setToolbarState(ToolbarState.LOADING) - // toolbar.js is served from the PostHog CDN, this has a TTL of 24 hours. - // the toolbar asset includes a rotating "token" that is valid for 5 minutes. - const fiveMinutesInMillis = 5 * 60 * 1000 - // this ensures that we bust the cache periodically - const timestampToNearestFiveMinutes = Math.floor(Date.now() / fiveMinutesInMillis) * fiveMinutesInMillis - - this.instance.requestRouter.loadScript(`/static/toolbar.js?t=${timestampToNearestFiveMinutes}`, (err) => { + assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'toolbar', (err) => { if (err) { logger.error('Failed to load toolbar', err) this.setToolbarState(ToolbarState.UNINITIALIZED) diff --git a/src/extensions/tracing-headers.ts b/src/extensions/tracing-headers.ts index 6ed536049..4ce7864de 100644 --- a/src/extensions/tracing-headers.ts +++ b/src/extensions/tracing-headers.ts @@ -1,7 +1,6 @@ import { PostHog } from '../posthog-core' import { assignableWindow } from '../utils/globals' import { logger } from '../utils/logger' -import Config from '../config' import { isUndefined } from '../utils/type-utils' const LOGGER_PREFIX = '[TRACING-HEADERS]' @@ -18,7 +17,7 @@ export class TracingHeaders { cb() } - this.instance.requestRouter.loadScript(`/static/tracing-headers.js?v=${Config.LIB_VERSION}`, (err) => { + assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'tracing-headers', (err) => { if (err) { return logger.error(LOGGER_PREFIX + ' failed to load script', err) } diff --git a/src/extensions/web-vitals/index.ts b/src/extensions/web-vitals/index.ts index ea5987e38..1d966c08d 100644 --- a/src/extensions/web-vitals/index.ts +++ b/src/extensions/web-vitals/index.ts @@ -4,7 +4,6 @@ import { logger } from '../../utils/logger' import { isBoolean, isNullish, isNumber, isObject, isUndefined } from '../../utils/type-utils' import { WEB_VITALS_ALLOWED_METRICS, WEB_VITALS_ENABLED_SERVER_SIDE } from '../../constants' import { assignableWindow, window } from '../../utils/globals' -import Config from '../../config' type WebVitalsMetricCallback = (metric: any) => void @@ -91,8 +90,7 @@ export class WebVitalsAutocapture { // already loaded cb() } - - this.instance.requestRouter.loadScript(`/static/web-vitals.js?v=${Config.LIB_VERSION}`, (err) => { + assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'web-vitals', (err) => { if (err) { logger.error(LOGGER_PREFIX + ' failed to load script', err) return diff --git a/src/posthog-surveys.ts b/src/posthog-surveys.ts index d7a9f0320..21b85dd0e 100644 --- a/src/posthog-surveys.ts +++ b/src/posthog-surveys.ts @@ -80,7 +80,8 @@ export class PostHogSurveys { if (this._surveyEventReceiver == null) { this._surveyEventReceiver = new SurveyEventReceiver(this.instance) } - this.instance.requestRouter.loadScript('/static/surveys.js', (err) => { + + assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'surveys', (err) => { if (err) { return logger.error(LOGGER_PREFIX, 'Could not load surveys script', err) } @@ -102,6 +103,7 @@ export class PostHogSurveys { } const existingSurveys = this.instance.get_property(SURVEYS) + if (!existingSurveys || forceReload) { this.instance._send_request({ url: this.instance.requestRouter.endpointFor( diff --git a/src/utils/globals.ts b/src/utils/globals.ts index 294866c1a..5400083d4 100644 --- a/src/utils/globals.ts +++ b/src/utils/globals.ts @@ -1,6 +1,6 @@ +import type { PostHog } from '../posthog-core' import { SessionIdManager } from '../sessionid' import { ErrorEventArgs, ErrorProperties, Properties } from '../types' -import { PostHog } from '../posthog-core' /* * Global helpers to protect access to browser globals in a way that is safer for different targets @@ -19,7 +19,24 @@ const win: (Window & typeof globalThis) | undefined = typeof window !== 'undefin * This is our contract between (potentially) lazily loaded extensions and the SDK * changes to this interface can be breaking changes for users of the SDK */ -interface PosthogExtensions { + +export type PostHogExtensionKind = + | 'toolbar' + | 'exception-autocapture' + | 'web-vitals' + | 'recorder' + | 'tracing-headers' + | 'surveys' + +interface PostHogExtensions { + loadExternalDependency?: ( + posthog: PostHog, + kind: PostHogExtensionKind, + callback: (error?: string | Event, event?: Event) => void + ) => void + + loadSiteApp?: (posthog: PostHog, appUrl: string, callback: (error?: string | Event, event?: Event) => void) => void + parseErrorAsProperties?: ([event, source, lineno, colno, error]: ErrorEventArgs) => ErrorProperties errorWrappingFunctions?: { wrapOnError: (captureFn: (props: Properties) => void) => () => void @@ -58,7 +75,7 @@ export const userAgent = navigator?.userAgent export const assignableWindow: Window & typeof globalThis & Record & { - __PosthogExtensions__?: PosthogExtensions + __PosthogExtensions__?: PostHogExtensions } = win ?? ({} as any) export { win as window } diff --git a/src/utils/request-router.ts b/src/utils/request-router.ts index da635f78b..4b0433183 100644 --- a/src/utils/request-router.ts +++ b/src/utils/request-router.ts @@ -1,6 +1,4 @@ import { PostHog } from '../posthog-core' -import { document } from '../utils/globals' -import { logger } from './logger' /** * The request router helps simplify the logic to determine which endpoints should be called for which things @@ -85,38 +83,4 @@ export class RequestRouter { return `https://${this.region}.${suffix}` } } - - loadScript(scriptUrlToLoad: string, callback: (error?: string | Event, event?: Event) => void): void { - if (this.instance.config.disable_external_dependency_loading) { - logger.warn(`${scriptUrlToLoad} was requested but loading of external scripts is disabled.`) - return callback('Loading of external scripts is disabled') - } - - const url = scriptUrlToLoad[0] === '/' ? this.endpointFor('assets', scriptUrlToLoad) : scriptUrlToLoad - - const addScript = () => { - if (!document) { - return callback('document not found') - } - const scriptTag = document.createElement('script') - scriptTag.type = 'text/javascript' - scriptTag.src = url - scriptTag.onload = (event) => callback(undefined, event) - scriptTag.onerror = (error) => callback(error) - - const scripts = document.querySelectorAll('body > script') - if (scripts.length > 0) { - scripts[0].parentNode?.insertBefore(scriptTag, scripts[0]) - } else { - // In exceptional situations this call might load before the DOM is fully ready. - document.body.appendChild(scriptTag) - } - } - - if (document?.body) { - addScript() - } else { - document?.addEventListener('DOMContentLoaded', addScript) - } - } }