diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 819d12966..1e04cb32b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,11 @@ name: Run Autumn Tests on: + workflow_dispatch: push: branches: - main - + - test/cicd env: DATABASE_URL: ${{ secrets.SUPABASE_URL }} UNIT_TEST_AUTUMN_SECRET_KEY: ${{ secrets.AUTUMN_KEY }} @@ -16,8 +17,7 @@ env: LOCALTUNNEL_RESERVED_KEY: ${{ secrets.LOCALTUNNEL_RESERVED_KEY }} STRIPE_WEBHOOK_URL: ${{ secrets.STRIPE_WEBHOOK_URL }} BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }} - BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }} - BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }} + HYPERBROWSER_API_KEY: ${{ secrets.HYPERBROWSER_API_KEY }} jobs: test: @@ -43,52 +43,52 @@ jobs: - name: Setup CI run: pnpm run setupci - - name: Install Chrome Dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - ca-certificates \ - fonts-liberation \ - libasound2 \ - libatk-bridge2.0-0 \ - libatk1.0-0 \ - libc6 \ - libcairo2 \ - libcups2 \ - libdbus-1-3 \ - libexpat1 \ - libfontconfig1 \ - libgbm1 \ - libgcc1 \ - libgdk-pixbuf2.0-0 \ - libglib2.0-0 \ - libgtk-3-0 \ - libnspr4 \ - libnss3 \ - libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libstdc++6 \ - libx11-6 \ - libx11-xcb1 \ - libxcb1 \ - libxcomposite1 \ - libxcursor1 \ - libxdamage1 \ - libxext6 \ - libxfixes3 \ - libxi6 \ - libxrandr2 \ - libxrender1 \ - libxss1 \ - libxtst6 \ - lsb-release \ - wget \ - xdg-utils + # - name: Install Chrome Dependencies + # run: | + # sudo apt-get update + # sudo apt-get install -y \ + # ca-certificates \ + # fonts-liberation \ + # libasound2 \ + # libatk-bridge2.0-0 \ + # libatk1.0-0 \ + # libc6 \ + # libcairo2 \ + # libcups2 \ + # libdbus-1-3 \ + # libexpat1 \ + # libfontconfig1 \ + # libgbm1 \ + # libgcc1 \ + # libgdk-pixbuf2.0-0 \ + # libglib2.0-0 \ + # libgtk-3-0 \ + # libnspr4 \ + # libnss3 \ + # libpango-1.0-0 \ + # libpangocairo-1.0-0 \ + # libstdc++6 \ + # libx11-6 \ + # libx11-xcb1 \ + # libxcb1 \ + # libxcomposite1 \ + # libxcursor1 \ + # libxdamage1 \ + # libxext6 \ + # libxfixes3 \ + # libxi6 \ + # libxrandr2 \ + # libxrender1 \ + # libxss1 \ + # libxtst6 \ + # lsb-release \ + # wget \ + # xdg-utils - - name: Install Chrome - run: | - cd server/ - npx puppeteer browsers install chrome + # - name: Install Chrome + # run: | + # cd server/ + # npx puppeteer browsers install chrome - name: Create .env file run: | @@ -103,8 +103,7 @@ jobs: echo "LOCALTUNNEL_RESERVED_KEY=${{ env.LOCALTUNNEL_RESERVED_KEY }}" >> .env echo "BETTER_AUTH_SECRET=${{ env.BETTER_AUTH_SECRET }}" >> .env echo "STRIPE_WEBHOOK_URL=${{ env.STRIPE_WEBHOOK_URL }}" >> .env - echo "BROWSERBASE_API_KEY=${{ env.BROWSERBASE_API_KEY }}" >> .env - echo "BROWSERBASE_PROJECT_ID=${{ env.BROWSERBASE_PROJECT_ID }}" >> .env + echo "HYPERBROWSER_API_KEY=${{ env.HYPERBROWSER_API_KEY }}" >> .env cat .env - name: Build shared @@ -132,6 +131,23 @@ jobs: cd server/ chmod +x ./shell/g1.sh ./shell/g1.sh + - name: Run G2 tests + run: | + cd server/ + chmod +x ./shell/g2.sh + ./shell/g2.sh + + - name: Run G3 tests + run: | + cd server/ + chmod +x ./shell/g3.sh + ./shell/g3.sh + + - name: Run G4 tests + run: | + cd server/ + chmod +x ./shell/g4.sh + ./shell/g4.sh - name: Message run: @@ -143,7 +159,4 @@ jobs: - name: Close Docker Containers if: always() - run: sudo docker compose -f docker-compose.unix.yml down - - - + run: sudo docker compose -f docker-compose.unix.yml down \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6c07ed7f..17fe7680f 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,10 @@ pull.sh # testing /coverage +# temporary files +**/.tmp/ +.tmp/ + # next.js /.next/ /out/ diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 67daae9b5..98fca125a 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -74,8 +74,7 @@ services: - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - BETTER_AUTH_URL=http://localhost:8080 - STRIPE_WEBHOOK_URL=${STRIPE_WEBHOOK_URL} - - BROWSERBASE_API_KEY=${BROWSERBASE_API_KEY} - - BROWSERBASE_PROJECT_ID=${BROWSERBASE_PROJECT_ID} + - HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY} depends_on: - shared restart: unless-stopped diff --git a/localtunnel-start.sh b/localtunnel-start.sh index edc0bfaab..a0b66f565 100644 --- a/localtunnel-start.sh +++ b/localtunnel-start.sh @@ -1,16 +1,16 @@ #!/bin/sh # Check for LOCAL_TUNNEL_RESERVED_KEY environment variable first -if [ -z "$LOCAL_TUNNEL_RESERVED_KEY" ]; then +if [ -z "$LOCALTUNNEL_RESERVED_KEY" ]; then # Read LOCAL_TUNNEL_RESERVED_KEY from .env file if env var not set if [ -f "/app/server/.env" ]; then - export $(cat /app/server/.env | grep LOCAL_TUNNEL_RESERVED_KEY | grep -v '^#') + export $(cat /app/server/.env | grep LOCALTUNNEL_RESERVED_KEY | grep -v '^#') fi fi # Set default subdomain if env var not found -if [ -z "$LOCAL_TUNNEL_RESERVED_KEY" ]; then - LOCAL_TUNNEL_RESERVED_KEY="autumn-dev" +if [ -z "$LOCALTUNNEL_RESERVED_KEY" ]; then + LOCALTUNNEL_RESERVED_KEY="autumn-dev" fi echo "Installing localtunnel..." @@ -19,4 +19,4 @@ bun install -g localtunnel echo "Server is ready! Starting localtunnel..." -lt --port 8080 --local-host server --subdomain ${LOCAL_TUNNEL_RESERVED_KEY} --print-requests \ No newline at end of file +lt --port 8080 --local-host server --subdomain ${LOCALTUNNEL_RESERVED_KEY} --print-requests \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a761dedc..a219d6554 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: '@axiomhq/pino': specifier: ^1.3.1 version: 1.3.1 + '@browserbasehq/sdk': + specifier: ^2.6.0 + version: 2.6.0 '@clerk/express': specifier: ^1.3.22 version: 1.7.5(express@4.21.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -63,6 +66,9 @@ importers: '@date-fns/utc': specifier: ^2.1.0 version: 2.1.0 + '@hyperbrowser/sdk': + specifier: ^0.54.0 + version: 0.54.0 '@hyperdx/node-opentelemetry': specifier: ^0.8.2 version: 0.8.2 @@ -213,6 +219,9 @@ importers: posthog-node: specifier: ^4.17.2 version: 4.18.0 + puppeteer-core: + specifier: ^24.14.0 + version: 24.14.0 react: specifier: ^18.2.0 version: 18.3.1 @@ -238,9 +247,6 @@ importers: specifier: ^3.25.23 version: 3.25.76 devDependencies: - '@browserbasehq/sdk': - specifier: ^2.6.0 - version: 2.6.0 '@types/chai': specifier: ^5.0.1 version: 5.2.2 @@ -283,12 +289,6 @@ importers: mocha: specifier: ^11.1.0 version: 11.7.1 - puppeteer: - specifier: ^24.2.0 - version: 24.12.0(typescript@5.7.3) - puppeteer-core: - specifier: ^24.14.0 - version: 24.14.0 react-email: specifier: 4.0.16 version: 4.0.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1231,6 +1231,9 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@hyperbrowser/sdk@0.54.0': + resolution: {integrity: sha512-QmbwMG6niqInlS0FfUWWzvv+DfyINHGnF8i/0oNuyyuiIdqSXnp+UG/74Ci+yz8QuyezmuKzBSp6dBXlVG0Glw==} + '@hyperdx/instrumentation-exception@0.1.0': resolution: {integrity: sha512-Jgk7JY5J07Mq9fgXApGVhSkS4+WdzzRcWLReAZhxgo46KShxE6w614mFqUSnuo+z6ghlehsy4ForViUfxrFyew==} peerDependencies: @@ -2658,11 +2661,6 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@puppeteer/browsers@2.10.5': - resolution: {integrity: sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==} - engines: {node: '>=18'} - hasBin: true - '@puppeteer/browsers@2.10.6': resolution: {integrity: sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==} engines: {node: '>=18'} @@ -4414,11 +4412,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chromium-bidi@5.1.0: - resolution: {integrity: sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==} - peerDependencies: - devtools-protocol: '*' - chromium-bidi@7.1.1: resolution: {integrity: sha512-L2BKQ0rSLADgbPMIdDh3wnYHs3EiUiMay2Sq0CTolheaADmWIf6Pe+T9LJRcnh5rcMz0U7MVk0cQVvKsGRMa1g==} peerDependencies: @@ -4565,15 +4558,6 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} - cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -4968,13 +4952,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -5524,9 +5501,6 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -5656,9 +5630,6 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -5868,9 +5839,6 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -6307,10 +6275,6 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - parseley@0.12.1: resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} @@ -6524,19 +6488,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - puppeteer-core@24.12.0: - resolution: {integrity: sha512-VrPXPho5Q90Ao86FwJVb+JeAF2Tf41wOTGg8k2SyQJePiJ6hJ5iujYpmP+bmhlb6o+J26bQYRDPOYXP7ALWcxQ==} - engines: {node: '>=18'} - puppeteer-core@24.14.0: resolution: {integrity: sha512-NO9XpCl+i8oB0zJp81iPhzMo2QK8/JTj4ramSvTpGCo9CPCNo4AZ8qVOGpSgXzlcOfOT3VHOkzTfPo08GOE5jA==} engines: {node: '>=18'} - puppeteer@24.12.0: - resolution: {integrity: sha512-MJtM71qex8h03bDBZTyPfSC7tfvDLILnWWl4rNdo3+HODiFZX+3yj/qLVwVu/gXoxQ7U8dNDKyFz4e8VBHdcmw==} - engines: {node: '>=18'} - hasBin: true - pvtsutils@1.3.6: resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} @@ -8207,6 +8162,15 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@hyperbrowser/sdk@0.54.0': + dependencies: + form-data: 4.0.3 + node-fetch: 2.7.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - encoding + '@hyperdx/instrumentation-exception@0.1.0(@opentelemetry/api@1.9.0)': dependencies: '@hyperdx/instrumentation-sentry-node': 0.1.0(@opentelemetry/api@1.9.0) @@ -10177,19 +10141,6 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@puppeteer/browsers@2.10.5': - dependencies: - debug: 4.4.1 - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.5.0 - semver: 7.7.2 - tar-fs: 3.1.0 - yargs: 17.7.2 - transitivePeerDependencies: - - bare-buffer - - supports-color - '@puppeteer/browsers@2.10.6': dependencies: debug: 4.4.1 @@ -12031,12 +11982,6 @@ snapshots: dependencies: readdirp: 4.1.2 - chromium-bidi@5.1.0(devtools-protocol@0.0.1464554): - dependencies: - devtools-protocol: 0.0.1464554 - mitt: 3.0.1 - zod: 3.25.76 - chromium-bidi@7.1.1(devtools-protocol@0.0.1464554): dependencies: devtools-protocol: 0.0.1464554 @@ -12176,15 +12121,6 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@9.0.0(typescript@5.7.3): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.7.3 - create-require@1.1.1: {} cron-parser@4.9.0: @@ -12471,12 +12407,6 @@ snapshots: entities@4.5.0: {} - env-paths@2.2.1: {} - - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -13155,8 +13085,6 @@ snapshots: ipaddr.js@1.9.1: {} - is-arrayish@0.2.1: {} - is-arrayish@0.3.2: optional: true @@ -13249,8 +13177,6 @@ snapshots: json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} - json-schema-traverse@0.4.1: {} json-schema@0.4.0: {} @@ -13467,8 +13393,6 @@ snapshots: lightningcss-win32-x64-msvc: 1.30.1 optional: true - lines-and-columns@1.2.4: {} - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -13920,13 +13844,6 @@ snapshots: dependencies: callsites: 3.1.0 - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - parseley@0.12.1: dependencies: leac: 0.6.0 @@ -14151,20 +14068,6 @@ snapshots: punycode@2.3.1: {} - puppeteer-core@24.12.0: - dependencies: - '@puppeteer/browsers': 2.10.5 - chromium-bidi: 5.1.0(devtools-protocol@0.0.1464554) - debug: 4.4.1 - devtools-protocol: 0.0.1464554 - typed-query-selector: 2.12.0 - ws: 8.18.3 - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - utf-8-validate - puppeteer-core@24.14.0: dependencies: '@puppeteer/browsers': 2.10.6 @@ -14179,21 +14082,6 @@ snapshots: - supports-color - utf-8-validate - puppeteer@24.12.0(typescript@5.7.3): - dependencies: - '@puppeteer/browsers': 2.10.5 - chromium-bidi: 5.1.0(devtools-protocol@0.0.1464554) - cosmiconfig: 9.0.0(typescript@5.7.3) - devtools-protocol: 0.0.1464554 - puppeteer-core: 24.12.0 - typed-query-selector: 2.12.0 - transitivePeerDependencies: - - bare-buffer - - bufferutil - - supports-color - - typescript - - utf-8-validate - pvtsutils@1.3.6: dependencies: tslib: 2.8.1 diff --git a/server/package.json b/server/package.json index b786b7884..96a2beb62 100644 --- a/server/package.json +++ b/server/package.json @@ -18,6 +18,7 @@ "cron": "tsx src/cron.ts", "cron:start": "node --no-deprecation dist/src/cron.js", "testa-parallel": "mocha 'tests/alex/00_setup.ts' && mocha --parallel --timeout 10000000 'tests/alex/**/*.ts' --ignore 'tests/alex/00_setup.ts'", + "testa-browser": "mocha 'tests/alex/00_setup.ts' && mocha --timeout 10000000 'tests/alex/**/*.ts' --ignore 'tests/alex/00_setup.ts'", "testa": "mocha 'tests/alex/00_setup.ts' && mocha --timeout 10000000 'tests/alex/01_free.ts' --ignore 'tests/alex/00_setup.ts'", "test": "mocha --timeout 10000000 'tests/**/*.ts' --ignore 'tests/alex/**/*.ts'", "test-custom": "mocha --timeout 1000000 'tests/01_product.ts'", @@ -48,10 +49,12 @@ "@anthropic-ai/sdk": "^0.32.1", "@autumn/shared": "workspace:*", "@axiomhq/pino": "^1.3.1", + "@browserbasehq/sdk": "^2.6.0", "@clerk/express": "^1.3.22", "@clickhouse/client": "^1.11.2", "@date-fns/tz": "^1.2.0", "@date-fns/utc": "^2.1.0", + "@hyperbrowser/sdk": "^0.54.0", "@hyperdx/node-opentelemetry": "^0.8.2", "@logtail/node": "^0.5.2", "@opentelemetry/api": "^1.9.0", @@ -102,6 +105,7 @@ "pino-pretty": "^13.0.0", "postgres": "^3.4.7", "posthog-node": "^4.17.2", + "puppeteer-core": "^24.14.0", "react": "^18.2.0", "recaseai": "^0.0.37", "resend": "^4.1.1", @@ -126,7 +130,6 @@ "cross-env": "^7.0.3", "drizzle-kit": "^0.31.1", "mocha": "^11.1.0", - "puppeteer": "^24.2.0", "react-email": "4.0.16", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", diff --git a/server/shell/g1.sh b/server/shell/g1.sh index 5149a001b..954e550e2 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -7,4 +7,6 @@ MOCHA_PARALLEL=true $MOCHA_SETUP \ && $MOCHA_CMD \ 'tests/attach/basic/*.ts' \ 'tests/attach/upgrade/*.ts' \ -'tests/attach/downgrade/*.ts' \ No newline at end of file +'tests/attach/downgrade/*.ts' + +# 'tests/attach/basic/basic2.ts' \ \ No newline at end of file diff --git a/server/shell/g2.sh b/server/shell/g2.sh index 083ef1294..5ae2b94a7 100755 --- a/server/shell/g2.sh +++ b/server/shell/g2.sh @@ -3,7 +3,12 @@ # Source shared configuration source "$(dirname "$0")/config.sh" -MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \ +# If contains setup then run $MOCHA_SETUP +if [[ "$1" == *"setup"* ]]; then + MOCHA_PARALLEL=true $MOCHA_SETUP +fi + +$MOCHA_CMD \ 'tests/attach/migrations/*.ts' \ 'tests/attach/newVersion/*.ts' \ 'tests/attach/upgradeOld/*.ts' \ diff --git a/server/shell/g3.sh b/server/shell/g3.sh index 7b6a2f1d3..8fb17499d 100755 --- a/server/shell/g3.sh +++ b/server/shell/g3.sh @@ -3,7 +3,11 @@ # Source shared configuration source "$(dirname "$0")/config.sh" -MOCHA_PARALLEL=true $MOCHA_SETUP +# MOCHA_PARALLEL=true $MOCHA_SETUP + +if [[ "$1" == *"setup"* ]]; then + MOCHA_PARALLEL=true $MOCHA_SETUP +fi $MOCHA_CMD 'tests/contUse/entities/*.ts' diff --git a/server/shell/g4.sh b/server/shell/g4.sh index 3a89df507..b5a362206 100755 --- a/server/shell/g4.sh +++ b/server/shell/g4.sh @@ -3,7 +3,10 @@ # Source shared configuration source "$(dirname "$0")/config.sh" -MOCHA_PARALLEL=true $MOCHA_SETUP +# MOCHA_PARALLEL=true $MOCHA_SETUP +if [[ "$1" == *"setup"* ]]; then + MOCHA_PARALLEL=true $MOCHA_SETUP +fi $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ 'tests/advanced/coupons/*.ts' \ diff --git a/server/src/internal/analytics/AnalyticsService.ts b/server/src/internal/analytics/AnalyticsService.ts index 179dc414e..eabec3318 100644 --- a/server/src/internal/analytics/AnalyticsService.ts +++ b/server/src/internal/analytics/AnalyticsService.ts @@ -50,7 +50,7 @@ export class AnalyticsService { const { clickhouseClient, org, env } = req; const query = ` - select count(*), event_name + select count(*) as count, event_name from org_events_view(org_id={org_id:String}, org_slug='', env={env:String}) where timestamp >= NOW() - INTERVAL '1 month' group by event_name @@ -68,7 +68,10 @@ export class AnalyticsService { const resultJson = await result.json(); - return resultJson.data.map((row: any) => row.event_name); + return { + eventNames: resultJson.data.map((row: any) => row.event_name), + result: resultJson, + }; } static async getTopUser({ req }: { req: ExtendedRequest }) { diff --git a/server/src/internal/analytics/internalAnalyticsRouter.ts b/server/src/internal/analytics/internalAnalyticsRouter.ts index f48cde0af..da748837a 100644 --- a/server/src/internal/analytics/internalAnalyticsRouter.ts +++ b/server/src/internal/analytics/internalAnalyticsRouter.ts @@ -32,9 +32,11 @@ analyticsRouter.get("/event_names", async (req: any, res: any) => action: CacheType.TopEvents, key: `${org.id}_${env}`, fn: async () => { - return await AnalyticsService.getTopEventNames({ + const res = await AnalyticsService.getTopEventNames({ req, }); + + return res?.eventNames; }, }); @@ -75,9 +77,11 @@ const getTopEvents = async ({ req }: { req: ExtendedRequest }) => { action: CacheType.TopEvents, key: `${org.id}_${env}`, fn: async () => { - return await AnalyticsService.getTopEventNames({ + const res = await AnalyticsService.getTopEventNames({ req, }); + + return res?.eventNames; }, }); diff --git a/server/src/internal/api/trmnl/trmnlRouter.ts b/server/src/internal/api/trmnl/trmnlRouter.ts index 42be7695d..eb55a3833 100644 --- a/server/src/internal/api/trmnl/trmnlRouter.ts +++ b/server/src/internal/api/trmnl/trmnlRouter.ts @@ -17,31 +17,37 @@ trmnlRouter.post("/screen", async (req: any, res: any) => res, action: "generate trmnl screen", handler: async () => { - let topEvent: string[] = await AnalyticsService.getTopEventNames({ + let { result }: any = await AnalyticsService.getTopEventNames({ req, limit: 1, }); - if (!topEvent || topEvent.length == 0) { - topEvent = ["Unknown"]; - } + let topEvent = "Unknown"; + if (result && result.data.length > 0) { + topEvent = result.data[0].event_name; + } - let feature = req.features.find((feature: Feature) => { - return feature.id === topEvent[0] || - (feature.config?.filters?.flatMap((x: { value: string[] }) => [...x.value]) && - feature.config.filters.flatMap((x: { value: string[] }) => [...x.value]).includes(topEvent[0])); - }); + let feature = req.features.find((feature: Feature) => { + return ( + feature.id === topEvent || + (feature.config?.filters?.flatMap((x: { value: string[] }) => [ + ...x.value, + ]) && + feature.config.filters + .flatMap((x: { value: string[] }) => [...x.value]) + .includes(topEvent)) + ); + }); - let featureName = getFeatureName({ - feature, - plural: true - }); + let featureName = getFeatureName({ + feature, + plural: true, + }); - let totalEvents: number | string = - await AnalyticsService.getTotalEvents({ - req, - eventName: topEvent[0], - }); + let totalEvents: number | string = await AnalyticsService.getTotalEvents({ + req, + eventName: topEvent, + }); if (!totalEvents) { totalEvents = "Unknown"; @@ -63,7 +69,7 @@ trmnlRouter.post("/screen", async (req: any, res: any) => let results = await AnalyticsService.getTimeseriesEvents({ req, params: { - event_names: [topEvent[0]], + event_names: [topEvent], interval: "30d", }, aggregateAll: true, @@ -82,23 +88,23 @@ trmnlRouter.post("/screen", async (req: any, res: any) => }; } - // console.log({ - // rowData: `[${results.data.map((row: any) => `['${row.period}', ${row[topEvent[0] + "_count"]}]`).join(",")}]`, - // revenue: numberWithCommas(monthlyRevenue.total_payment_volume), - // totalEvent: numberWithCommas(totalEvents), - // totalCustomers: numberWithCommas(totalCustomers), - // topEvent: topEvent[0], - // }); - topEvent[0] = topEvent[0].replace("-", "_"); - res.status(200).json({ - rowData: `[${results.data.map((row: any) => `['${row.period}', ${row[topEvent[0] + "_count"]}]`).join(",")}]`, - revenue: numberWithCommas(monthlyRevenue.total_payment_volume), - totalEvents: numberWithCommas(totalEvents), - totalCustomers: numberWithCommas(totalCustomers), - topEvent: featureName, - }); - }, - }) + // console.log({ + // rowData: `[${results.data.map((row: any) => `['${row.period}', ${row[topEvent[0] + "_count"]}]`).join(",")}]`, + // revenue: numberWithCommas(monthlyRevenue.total_payment_volume), + // totalEvent: numberWithCommas(totalEvents), + // totalCustomers: numberWithCommas(totalCustomers), + // topEvent: topEvent[0], + // }); + topEvent = topEvent.replace("-", "_"); + res.status(200).json({ + rowData: `[${results.data.map((row: any) => `['${row.period}', ${row[topEvent + "_count"]}]`).join(",")}]`, + revenue: numberWithCommas(monthlyRevenue.total_payment_volume), + totalEvents: numberWithCommas(totalEvents), + totalCustomers: numberWithCommas(totalCustomers), + topEvent: featureName, + }); + }, + }) ); export { trmnlRouter }; diff --git a/server/src/internal/mainRouter.ts b/server/src/internal/mainRouter.ts index dd9c16c91..7c0e0d836 100644 --- a/server/src/internal/mainRouter.ts +++ b/server/src/internal/mainRouter.ts @@ -43,6 +43,7 @@ const trmnlLimiter = rateLimit({ limit: process.env.NODE_ENV === "development" ? 1000 : 10, standardHeaders: "draft-8", legacyHeaders: false, + validate: { xForwardedForHeader: false }, }); mainRouter.use("/trmnl", trmnlLimiter, trmnlAuthMiddleware, trmnlRouter); diff --git a/server/src/middleware/trmnlAuthMiddleware.ts b/server/src/middleware/trmnlAuthMiddleware.ts index d9abcd2d5..fcd9aa43f 100644 --- a/server/src/middleware/trmnlAuthMiddleware.ts +++ b/server/src/middleware/trmnlAuthMiddleware.ts @@ -32,15 +32,16 @@ export const trmnlAuthMiddleware = async ( }); } + req.env = req.headers["env"] || AppEnv.Live; const features = await FeatureService.list({ db: req.db, orgId: fileJson[trmnlId], - env: req.headers["env"] || AppEnv.Live, + env: req.env, }); req.org = { id: fileJson[trmnlId], - env: req.headers["env"] || AppEnv.Live, + env: req.env, }; req.features = features; diff --git a/server/src/utils/scriptUtils/genScriptUtils.ts b/server/src/utils/scriptUtils/genScriptUtils.ts new file mode 100644 index 000000000..1933b9246 --- /dev/null +++ b/server/src/utils/scriptUtils/genScriptUtils.ts @@ -0,0 +1,21 @@ +import csv from "csv-parser"; +import fs from "fs"; + +export const parseCsv = (path: string) => { + return new Promise((resolve, reject) => { + const stream = fs.createReadStream(path); + const results: any[] = []; + let headers: string[] = []; + stream + .pipe(csv({ separator: "," })) + .on("data", (data) => { + if (headers.length === 0) { + headers = Object.keys(data); + } else { + results.push(data); + } + }) + .on("end", () => resolve(results)) + .on("error", (error) => reject(error)); + }) as Promise; +}; diff --git a/server/tests/00_setup.ts b/server/tests/00_setup.ts index 6d7edf2f6..e84ca5cfe 100644 --- a/server/tests/00_setup.ts +++ b/server/tests/00_setup.ts @@ -19,6 +19,11 @@ import { initDrizzle } from "@/db/initDrizzle.js"; const ORG_SLUG = process.env.TESTS_ORG!; const DEFAULT_ENV = AppEnv.Sandbox; +import { Hyperbrowser } from "@hyperbrowser/sdk"; +const hyperbrowser = new Hyperbrowser({ + apiKey: process.env.HYPERBROWSER_API_KEY, +}); + describe("Initialize org for tests", () => { it("should initialize org", async function () { this.timeout(1000000000); diff --git a/server/tests/attach/basic/basic2.ts b/server/tests/attach/basic/basic2.ts index 04be9b43f..9ae9f6068 100644 --- a/server/tests/attach/basic/basic2.ts +++ b/server/tests/attach/basic/basic2.ts @@ -66,11 +66,11 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => { const res: any = await AutumnCli.entitled( customerId, - entitlement.feature_id!, + entitlement.feature_id! ); const entBalance = res!.balances.find( - (b: any) => b.feature_id === entitlement.feature_id, + (b: any) => b.feature_id === entitlement.feature_id ); try { @@ -91,205 +91,4 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => { } } }); - - return; - - const oneTimeBillingUnits = - products.oneTimeAddOnMetered1.prices[0].config.billing_units; - const monthlyBillingUnits = - products.monthlyAddOnMetered1.prices[0].config.billing_units; - - describe("One time add on (force checkout)", () => { - // PURCHASE ONE TIME ADD ON - - it("POST /attach -- attaching one time add on (force checkout) [no quantity passed in]", async function () { - try { - for (let i = 0; i < oneTimePurchaseCount; i++) { - const res = await AutumnCli.attach({ - customerId: customerId, - productId: products.oneTimeAddOnMetered1.id, - forceCheckout: true, - }); - - await completeCheckoutForm(res.checkout_url, oneTimeOverrideQuantity); - await timeout(10000); // for webhook to be processed - console.log(` ${chalk.greenBright("Attached one time add on")}`); - } - } catch (error) { - console.group(); - console.group(); - console.log("Failed to attach one time add on"); - console.log("Error data:", error); - console.groupEnd(); - console.groupEnd(); - process.exit(1); - } - }); - - // TODO: Attach one time add on again (with quantity?) - - it("GET /customers/:id -- checking product & entitlements (one time add on)", async function () { - const cusRes = await AutumnCli.getCustomer(customerId); - - // 1. Metered1 balance should be pro + one time add on - - // Fetch balance - const addOnBalance = cusRes.entitlements.find( - (e: any) => - e.feature_id === features.metered1.id && - e.interval == - products.oneTimeAddOnMetered1.entitlements.metered1.interval, - ); - - const expectedAmt = - (oneTimeOverrideQuantity || oneTimeQuantity) * - oneTimeBillingUnits * - oneTimePurchaseCount; - - try { - assert.equal(addOnBalance!.balance, expectedAmt); - assert.equal(cusRes.add_ons.length, 1); - assert.equal(cusRes.add_ons[0].id, products.oneTimeAddOnMetered1.id); - assert.equal(cusRes.invoices.length, 1 + oneTimePurchaseCount); - } catch (error) { - console.group(); - console.group(); - console.log("GET customer, balances failed"); - console.log( - "Add on entitlement:", - products.oneTimeAddOnMetered1.entitlements.metered1, - ); - console.log("Customer entitlements:", cusRes.entitlements); - - console.groupEnd(); - console.groupEnd(); - throw error; - } - }); - - it("GET /entitled -- checking entitled for metered1", async function () { - const res: any = await AutumnCli.entitled( - customerId, - features.metered1.id, - ); - - expect(res!.allowed).to.be.true; - - // pro metered1 - const proMetered1Amt = products.pro.entitlements.metered1.allowance; - - const addOnBalance = res!.balances.find( - (b: any) => b.feature_id === features.metered1.id, - ); - - expect(res!.allowed).to.be.true; - expect(addOnBalance!.balance).to.equal( - proMetered1Amt! + - (oneTimeOverrideQuantity || oneTimeQuantity) * - oneTimeBillingUnits * - oneTimePurchaseCount, - ); - }); - }); - - // PURCHASE MONTHLY ADD ON - describe("Monthly add on", () => { - it("POST /attach -- attaching monthly add on", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: products.monthlyAddOnMetered1.id, - forceCheckout: false, - options: [ - { - feature_id: features.metered1.id, - quantity: monthlyQuantity * monthlyBillingUnits, - }, - ], - }); - await timeout(10000); - - console.log(` ${chalk.greenBright("Attached monthly top up")}`); - }); - - it("GET /customers/:id -- checking product & entitlements (monthly add on)", async function () { - const cusRes = await AutumnCli.getCustomer(customerId); - - // 1. Metered1 balance should be pro + one time add on - const proMetered1 = products.pro.entitlements.metered1.allowance; - - // Fetch balance - const monthlyMetered1Balance = cusRes.entitlements.find( - (e: any) => - e.feature_id === features.metered1.id && - e.interval == - products.monthlyAddOnMetered1.entitlements.metered1.interval, - ); - - try { - assert.equal( - monthlyMetered1Balance!.balance, - proMetered1! + monthlyQuantity * monthlyBillingUnits, - ); - - assert.equal(cusRes.add_ons.length, 2); - const monthlyAddOnId = cusRes.add_ons.find( - (a: any) => a.id === products.monthlyAddOnMetered1.id, - ); - - assert.exists(monthlyAddOnId); - expect(cusRes.invoices.length).to.equal(2 + oneTimePurchaseCount); - } catch (error) { - console.group(); - console.group(); - console.log("GET customer, balances failed"); - console.log( - "Add on entitlement:", - products.monthlyAddOnMetered1.entitlements.metered1, - ); - console.log("Customer entitlements:", cusRes.entitlements); - - console.groupEnd(); - console.groupEnd(); - throw error; - } - }); - - it("GET /entitled -- checking entitlements (monthly add on)", async function () { - const res: any = await AutumnCli.entitled( - customerId, - features.metered1.id, - ); - - const metered1Balance = res!.balances.find( - (b: any) => b.feature_id === features.metered1.id, - ); - - const proMetered1Amt = products.pro.entitlements.metered1.allowance; - const monthlyAddOnMetered1Amt = monthlyQuantity * monthlyBillingUnits; - - const oneTimeAddOnMetered1Amt = - (oneTimeOverrideQuantity || oneTimeQuantity) * - oneTimeBillingUnits * - oneTimePurchaseCount; - - try { - expect(metered1Balance!.balance).to.equal( - proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt, - ); - } catch (error) { - console.group(); - console.group(); - console.log("GET entitled, balances failed"); - - console.log("/entitled response:", res); - console.log("Pro metered1 amt:", proMetered1Amt); - console.log("Monthly add on metered1 amt:", monthlyAddOnMetered1Amt); - console.log("One time add on metered1 amt:", oneTimeAddOnMetered1Amt); - - console.groupEnd(); - console.groupEnd(); - throw error; - } - }); - }); }); diff --git a/server/tests/attach/basic/basic3.ts b/server/tests/attach/basic/basic3.ts index a2f2c2624..69d032bce 100644 --- a/server/tests/attach/basic/basic3.ts +++ b/server/tests/attach/basic/basic3.ts @@ -104,7 +104,7 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on await completeCheckoutForm( res.checkout_url, - oneTimeQuantity / oneTimeBillingUnits!, + oneTimeQuantity / oneTimeBillingUnits! ); await timeout(15000); } @@ -117,26 +117,26 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on (e: any) => e.feature_id === features.metered1.id && e.interval == - products.oneTimeAddOnMetered1.entitlements.metered1.interval, + products.oneTimeAddOnMetered1.entitlements.metered1.interval ); const expectedAmt = oneTimeQuantity * oneTimePurchaseCount; expect(addOnBalance!.balance).to.equal( expectedAmt, - "add on balance should be correct", + "add on balance should be correct" ); expect(cusRes.add_ons).to.have.lengthOf( 1, - "should only have one add on product after two purchases (since they combine)", + "should only have one add on product after two purchases (since they combine)" ); expect(cusRes.add_ons[0].id).to.equal( oneTime.id, - "add on product should exist", + "add on product should exist" ); expect(cusRes.invoices.length).to.equal( 1 + oneTimePurchaseCount, - "invoices should be correct", + "invoices should be correct" ); }); @@ -147,12 +147,12 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on const proMetered1Amt = products.pro.entitlements.metered1.allowance; const addOnBalance = res!.balances.find( - (b: any) => b.feature_id === features.metered1.id, + (b: any) => b.feature_id === features.metered1.id ); expect(res!.allowed).to.be.true; expect(addOnBalance!.balance).to.equal( - proMetered1Amt! + oneTimeQuantity * oneTimePurchaseCount, + proMetered1Amt! + oneTimeQuantity * oneTimePurchaseCount ); }); }); diff --git a/server/tests/before.ts b/server/tests/before.ts index f81de2346..931f206ec 100644 --- a/server/tests/before.ts +++ b/server/tests/before.ts @@ -13,6 +13,11 @@ import { after } from "mocha"; const ORG_SLUG = process.env.TESTS_ORG!; const DEFAULT_ENV = AppEnv.Sandbox; +import { Hyperbrowser } from "@hyperbrowser/sdk"; +const hyperbrowser = new Hyperbrowser({ + apiKey: process.env.HYPERBROWSER_API_KEY, +}); + export const setupBefore = async (instance: any) => { const { db, client } = initDrizzle(); @@ -24,6 +29,11 @@ export const setupBefore = async (instance: any) => { const autumnSecretKey = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!; const autumn = new AutumnInt({ apiKey: autumnSecretKey }); + // console.log("instance.browserSession", instance.browserSession); + // if (!instance.browserSession) { + // instance.browserSession = await hyperbrowser.sessions.create(); + // } + const autumnJs = new AutumnJS({ secretKey: autumnSecretKey, url: "http://localhost:8080/v1", diff --git a/server/tests/browserSession.ts b/server/tests/browserSession.ts new file mode 100644 index 000000000..98724d881 --- /dev/null +++ b/server/tests/browserSession.ts @@ -0,0 +1,186 @@ +import { Hyperbrowser } from "@hyperbrowser/sdk"; +import * as fs from "fs"; +import * as path from "path"; + +const client = new Hyperbrowser({ + apiKey: process.env.HYPERBROWSER_API_KEY, +}); + +let sessionPromise: Promise | null = null; +let currentSession: any | null = null; + +// File paths for coordination across processes +const TEMP_DIR = path.join(process.cwd(), ".tmp"); +const SESSION_FILE = path.join(TEMP_DIR, "browser-session.json"); +const LOCK_FILE = path.join(TEMP_DIR, "browser-session.lock"); + +// Ensure temp directory exists +if (!fs.existsSync(TEMP_DIR)) { + fs.mkdirSync(TEMP_DIR, { recursive: true }); +} + +export const getBrowserSession = async () => { + // If we already have a session in memory, return it + if (currentSession) { + return currentSession; + } + + // Check if another process has already created a session + const existingSession = loadSessionFromFile(); + if (existingSession) { + currentSession = existingSession; + console.log("Using existing browser session:", existingSession.id); + return existingSession; + } + + // If session creation is already in progress, wait for it + if (sessionPromise) { + return await sessionPromise; + } + + // Try to acquire lock and create session + sessionPromise = createSessionWithLock(); + + try { + currentSession = await sessionPromise; + return currentSession; + } catch (error) { + // Reset promise on failure so we can retry + sessionPromise = null; + throw error; + } +}; + +const createSessionWithLock = async (): Promise => { + // Try to acquire lock + const lockAcquired = await acquireLock(); + + if (!lockAcquired) { + // Another process is creating the session, wait for it + console.log("Waiting for another process to create browser session..."); + return await waitForSession(); + } + + try { + // Double-check if session was created while we were acquiring lock + const existingSession = loadSessionFromFile(); + if (existingSession) { + console.log( + "Session was created by another process:", + existingSession.id + ); + return existingSession; + } + + // Create new session + console.log("Creating new browser session..."); + const session = await client.sessions.create(); + console.log("Browser session created successfully:", session.id); + + // Save session to file for other processes + saveSessionToFile(session); + + return session; + } finally { + // Always release lock + releaseLock(); + } +}; + +const acquireLock = async (): Promise => { + try { + // Try to create lock file exclusively + fs.writeFileSync(LOCK_FILE, process.pid.toString(), { flag: "wx" }); + return true; + } catch (error) { + // Lock file exists, another process has the lock + return false; + } +}; + +const releaseLock = () => { + try { + if (fs.existsSync(LOCK_FILE)) { + fs.unlinkSync(LOCK_FILE); + } + } catch (error) { + // Ignore errors when releasing lock + console.warn("Warning: Could not release lock file:", error); + } +}; + +const saveSessionToFile = (session: any) => { + try { + fs.writeFileSync(SESSION_FILE, JSON.stringify(session)); + } catch (error) { + console.error("Failed to save session to file:", error); + } +}; + +const loadSessionFromFile = (): any | null => { + try { + if (fs.existsSync(SESSION_FILE)) { + const sessionData = fs.readFileSync(SESSION_FILE, "utf-8"); + return JSON.parse(sessionData); + } + } catch (error) { + // If file is corrupted or doesn't exist, ignore + console.warn("Could not load session from file:", error); + } + return null; +}; + +const waitForSession = async (): Promise => { + // Poll for session file to appear + for (let i = 0; i < 30; i++) { + // Wait up to 30 seconds + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second + + const session = loadSessionFromFile(); + if (session) { + console.log("Found session created by another process:", session.id); + return session; + } + } + + throw new Error( + "Timeout waiting for browser session to be created by another process" + ); +}; + +// Reset session state (call this between test suites if needed) +export const resetBrowserSession = () => { + console.log("Resetting browser session state..."); + currentSession = null; + sessionPromise = null; + + // Clean up files + try { + if (fs.existsSync(SESSION_FILE)) { + fs.unlinkSync(SESSION_FILE); + } + if (fs.existsSync(LOCK_FILE)) { + fs.unlinkSync(LOCK_FILE); + } + } catch (error) { + console.warn("Warning: Could not clean up session files:", error); + } +}; + +// Cleanup on process exit +process.on("exit", () => { + if (currentSession) { + console.log("Process exiting, cleaning up browser session files..."); + releaseLock(); + } +}); + +process.on("SIGINT", () => { + resetBrowserSession(); + process.exit(0); +}); + +process.on("SIGTERM", () => { + resetBrowserSession(); + process.exit(0); +}); diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts index b762a4753..c53e93931 100644 --- a/server/tests/utils/setup.ts +++ b/server/tests/utils/setup.ts @@ -236,7 +236,7 @@ export const setupOrg = async ({ logger: console, }); - const allFeatures = await FeatureService.list({ db, orgId, env }); + // const allFeatures = await FeatureService.list({ db, orgId, env }); let org: Organization | null = null; let newFeatures: Feature[] = []; diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts index 8b47e01b7..7c8fb4259 100644 --- a/server/tests/utils/stripeUtils.ts +++ b/server/tests/utils/stripeUtils.ts @@ -1,4 +1,5 @@ -import puppeteer from "puppeteer"; +import "dotenv/config"; + import { timeout } from "./genUtils.js"; import { Stripe } from "stripe"; import { BillingInterval, Customer, FullProduct } from "@autumn/shared"; @@ -10,23 +11,36 @@ import { addWeeks, format, } from "date-fns"; +import puppeteer from "puppeteer-core"; +import Browserbase from "@browserbasehq/sdk"; const STRIPE_TEST_CLOCK_TIMING = 20000; // 30s -// const STRIPE_TEST_CLOCK_TIMING = 40000; // 30s +import { Hyperbrowser } from "@hyperbrowser/sdk"; + +const client = new Hyperbrowser({ + apiKey: process.env.HYPERBROWSER_API_KEY, +}); export const completeCheckoutForm = async ( url: string, overrideQuantity?: number, promoCode?: string ) => { - const browser = await puppeteer.launch({ - headless: true, - args: ["--no-sandbox", "--disable-setuid-sandbox"], + const session = await client.sessions.create(); + + const browser = await puppeteer.connect({ + browserWSEndpoint: session!.wsEndpoint, + defaultViewport: null, }); + + // const browser = await puppeteer.launch({ + // headless: true, + // args: ["--no-sandbox", "--disable-setuid-sandbox"], + // }); + try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size - await page.goto(url); // await page.waitForSelector("#payment-method-accordion-item-title-card"); diff --git a/vite/src/views/customers/customer/analytics/hooks/useTopEventNames.tsx b/vite/src/views/customers/customer/analytics/hooks/useTopEventNames.tsx index 3af865229..d5e500db7 100644 --- a/vite/src/views/customers/customer/analytics/hooks/useTopEventNames.tsx +++ b/vite/src/views/customers/customer/analytics/hooks/useTopEventNames.tsx @@ -1,4 +1,4 @@ -import { usePostSWR } from "@/services/useAxiosSwr"; +import { usePostSWR } from "@/services/useAxiosSwr.js"; import { ErrCode } from "@autumn/shared"; export const useTopEventNames = () => {