diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml new file mode 100644 index 00000000..30b33b91 --- /dev/null +++ b/.github/actions/install-playwright/action.yml @@ -0,0 +1,37 @@ +name: 'Install playwright' +description: 'Install Playwright browser binaries and OS dependencies' +runs: + using: 'composite' + steps: + - name: Get installed Playwright version + id: playwright-version + shell: bash + working-directory: ./packages/integration-tests + run: | + PLAYWRIGHT_VERSION=$(npm ls --json "@playwright/test" | jq --raw-output '.dependencies["@playwright/test"].version') + echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV + + - name: Set up path for Playwright cache + shell: bash + run: echo "PLAYWRIGHT_CACHE_PATH=$(if [[ $RUNNER_OS == 'macOS' ]]; then echo '~/Library/Caches/ms-playwright'; else echo '~/.cache/ms-playwright'; fi)" >> $GITHUB_ENV + + - name: Cache Playwright binaries + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + id: playwright-cache + with: + path: ${{ env.PLAYWRIGHT_CACHE_PATH }} + key: '${{ runner.os }}-playwright-cache-${{ env.PLAYWRIGHT_VERSION }}-splunk-otel-js-web-artifacts' + + - name: Install Playwright browser binaries + if: steps.playwright-cache.outputs.cache-hit != 'true' + shell: bash + run: | + npx playwright install --with-deps + npx playwright install chrome + npx playwright install msedge + npx playwright install webkit + + - name: Install Playwright OS dependencies + if: steps.playwright-cache.outputs.cache-hit != 'true' + shell: bash + run: npx playwright install-deps diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index a7fa6831..48baca63 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -38,3 +38,7 @@ jobs: uses: ./.github/actions/setup - name: Local integration tests using ${{ matrix.browser }} run: npm run test:integration:local:${{ matrix.browser }}:_execute + + playwright: + uses: ./.github/workflows/tests.yml + secrets: inherit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index beaa0aed..59528e15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,3 +82,7 @@ jobs: path: | packages/web/tests_output/ packages/web/logs/ + + playwright: + uses: ./.github/workflows/tests.yml + secrets: inherit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..a3bc3909 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,118 @@ +name: Playwright tests +on: + workflow_call: + +jobs: + playwright: + timeout-minutes: 60 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + browser: [chromium, firefox, chrome, edge] + name: Playwright Tests - ${{ matrix.browser }} + steps: + - name: Checkout + uses: actions/checkout@v4.2.1 + - uses: actions/setup-node@v4.0.3 + with: + node-version: '18' + cache: 'npm' + - run: npm ci + - run: npm run compile + + - name: Install playwright + uses: ./.github/actions/install-playwright + + - name: Run Playwright tests + run: | + npx playwright test --project=${{ matrix.browser }} + working-directory: ./packages/integration-tests + + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a + with: + name: blob-report-${{ matrix.browser }}-attempt-${{ github.run_attempt }} + path: packages/integration-tests/blob-report + retention-days: 1 + + playwright-macos: + timeout-minutes: 60 + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + browser: [webkit] + steps: + - name: Checkout + uses: actions/checkout@v4.2.1 + - uses: actions/setup-node@v4.0.3 + with: + node-version: '18' + cache: 'npm' + - run: npm ci + - run: npm run compile + + - name: Install playwright + uses: ./.github/actions/install-playwright + + - name: Run Playwright tests + run: | + npx playwright test --project=${{ matrix.browser }} + working-directory: ./packages/integration-tests + + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a + with: + name: blob-report-${{ matrix.browser }}-attempt-${{ github.run_attempt }} + path: packages/integration-tests/blob-report + retention-days: 1 + + merge-reports: + if: ${{ !cancelled() }} + needs: [playwright, playwright-macos] + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.2.1 + - uses: actions/setup-node@v4.0.3 + with: + node-version: '18' + cache: 'npm' + - run: npm ci + - run: npm run compile + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 + with: + path: packages/integration-tests/all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./all-blob-reports -c playwright.config.ts + working-directory: ./packages/integration-tests + + - name: Upload HTML report + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a + with: + name: playwright-report--attempt-${{ github.run_attempt }} + path: packages/integration-tests/playwright-report + retention-days: 14 + + check-failure: + needs: [playwright, playwright-macos, merge-reports] + runs-on: ubuntu-latest + if: ${{ always() }} + steps: + - name: Check if any playwright tests failed + run: | + if [ "${{ needs.playwright.result }}" != "success" ] || [ "${{ needs['playwright-macos'].result }}" != "success" ]; then + echo "One or more tests failed." + exit 1 + else + echo "All tests passed." + fi diff --git a/package-lock.json b/package-lock.json index 7b7698d3..b4430c3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.19.3", "workspaces": [ "packages/web", - "packages/session-recorder" + "packages/session-recorder", + "packages/integration-tests" ], "devDependencies": { "@aws-sdk/client-cloudfront": "^3.171.0", @@ -2919,6 +2920,390 @@ "fd-slicer2": "^1.2.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3960,6 +4345,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -5418,6 +5819,10 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true }, + "node_modules/@splunk/otel-js-web-integration-tests": { + "resolved": "packages/integration-tests", + "link": true + }, "node_modules/@splunk/otel-web": { "resolved": "packages/web", "link": true @@ -5580,6 +5985,17 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -5598,6 +6014,16 @@ "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", "dev": true }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -5618,18 +6044,58 @@ "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "dev": true }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5668,6 +6134,13 @@ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "dev": true }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mocha": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", @@ -5687,13 +6160,28 @@ } }, "node_modules/@types/node": { - "version": "20.14.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz", - "integrity": "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.8" } }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -5719,6 +6207,29 @@ "@types/ws": "*" } }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/shimmer": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz", @@ -6876,10 +7387,11 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6889,7 +7401,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -7988,10 +8500,11 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8037,6 +8550,7 @@ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, + "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -9009,6 +9523,45 @@ "node": ">=0.12" } }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -9438,6 +9991,7 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -9459,37 +10013,38 @@ "dev": true }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9509,6 +10064,16 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -9762,13 +10327,14 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -9784,15 +10350,27 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/find-cache-dir": { "version": "3.3.2", @@ -9968,6 +10546,7 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10022,7 +10601,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -10254,6 +10832,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-uri": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", @@ -12867,10 +13457,14 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge2": { "version": "1.4.1", @@ -14378,6 +14972,7 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -14716,10 +15311,11 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true, + "license": "MIT" }, "node_modules/pathval": { "version": "1.1.1", @@ -14849,6 +15445,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -15112,12 +15755,13 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -15478,6 +16122,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -15829,10 +16482,11 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -15857,6 +16511,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -15865,13 +16520,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -15883,7 +16540,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/sentence-case": { "version": "3.0.4", @@ -15906,20 +16564,31 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -17172,6 +17841,25 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -17484,9 +18172,10 @@ "dev": true }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -18281,6 +18970,24 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "packages/integration-tests": { + "name": "@splunk/otel-js-web-integration-tests", + "version": "0.19.3", + "license": "Apache-2.0", + "dependencies": { + "tsx": "^4.19.2" + }, + "devDependencies": { + "@playwright/test": "^1.48.2", + "@types/ejs": "^3.1.5", + "@types/express": "^5.0.0", + "@types/node": "^22.9.0", + "cors": "^2.8.5", + "ejs": "^3.1.10", + "express": "^4.21.1", + "on-headers": "^1.0.2" + } + }, "packages/session-recorder": { "name": "@splunk/otel-web-session-recorder", "version": "0.19.3", diff --git a/package.json b/package.json index a6872a12..9964276e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "--workspaces": "Hardcoded so npm runs workspaces commands in order", "workspaces": [ "packages/web", - "packages/session-recorder" + "packages/session-recorder", + "packages/integration-tests" ], "engines": { "--": "Versions required for development only (recent enough to use npm workspaces)", diff --git a/packages/integration-tests/.gitignore b/packages/integration-tests/.gitignore new file mode 100644 index 00000000..68c5d18f --- /dev/null +++ b/packages/integration-tests/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/packages/integration-tests/package-lock.json b/packages/integration-tests/package-lock.json new file mode 100644 index 00000000..77c4445a --- /dev/null +++ b/packages/integration-tests/package-lock.json @@ -0,0 +1,1697 @@ +{ + "name": "@splunk/otel-js-web-integration-tests", + "version": "0.19.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@splunk/otel-js-web-integration-tests", + "version": "0.19.1", + "license": "Apache-2.0", + "dependencies": { + "tsx": "^4.19.2" + }, + "devDependencies": { + "@playwright/test": "^1.48.2", + "@types/ejs": "^3.1.5", + "@types/express": "^5.0.0", + "@types/node": "^22.9.0", + "ejs": "^3.1.10", + "express": "^4.21.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json new file mode 100644 index 00000000..cdfcfac5 --- /dev/null +++ b/packages/integration-tests/package.json @@ -0,0 +1,30 @@ +{ + "name": "@splunk/otel-js-web-integration-tests", + "private": true, + "version": "0.19.3", + "repository": "github:signalfx/splunk-otel-js-browser", + "scripts": { + "server": "tsx src/server/server.ts", + "test": "npx playwright test", + "compile": "exit 0;" + }, + "author": "Splunk Observability Instrumentals Team ", + "license": "Apache-2.0", + "devDependencies": { + "@playwright/test": "^1.48.2", + "@types/ejs": "^3.1.5", + "@types/express": "^5.0.0", + "@types/node": "^22.9.0", + "cors": "^2.8.5", + "ejs": "^3.1.10", + "express": "^4.21.1", + "on-headers": "^1.0.2" + }, + "bugs": { + "url": "https://github.com/signalfx/splunk-otel-js-web/issues" + }, + "homepage": "https://github.com/signalfx/splunk-otel-js-web#readme", + "dependencies": { + "tsx": "^4.19.2" + } +} diff --git a/packages/integration-tests/playwright.config.ts b/packages/integration-tests/playwright.config.ts new file mode 100644 index 00000000..34d0a4e2 --- /dev/null +++ b/packages/integration-tests/playwright.config.ts @@ -0,0 +1,101 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { defineConfig, devices } from '@playwright/test' + +const REPORTERS = [] +if (process.env.CI) { + REPORTERS.push(['blob'], ['list']) +} else { + REPORTERS.push(['list'], ['html', { open: 'never' }]) +} + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './src/tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: REPORTERS, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'], headless: true }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'], headless: true }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'], headless: true }, + }, + + { + name: 'edge', + use: { + ...devices['Desktop Edge'], + channel: 'msedge', + headless: true, + }, + }, + + { + name: 'chrome', + use: { + ...devices['Desktop Chrome'], + channel: 'chrome', + headless: true, + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run server', + url: 'http://127.0.0.1:3000', + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/packages/integration-tests/src/pages/record-page.ts b/packages/integration-tests/src/pages/record-page.ts new file mode 100644 index 00000000..119a90ed --- /dev/null +++ b/packages/integration-tests/src/pages/record-page.ts @@ -0,0 +1,132 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { BrowserContext, Page } from 'playwright' +import { Span } from '@opentelemetry/exporter-zipkin/build/src/types' + +export class RecordPage { + receivedSpans: Span[] = [] + + get receivedErrorSpans() { + return this.receivedSpans.filter((span) => span.name === 'onerror') + } + + constructor( + private readonly page: Page, + private readonly context: BrowserContext, + ) {} + + changeVisibilityInTab = async (state: 'visible' | 'hidden') => { + await this.page.evaluate((stateInner) => { + Object.defineProperty(document, 'visibilityState', { value: stateInner, writable: true }) + Object.defineProperty(document, 'hidden', { value: Boolean(stateInner === 'hidden'), writable: true }) + + window.dispatchEvent(new Event('visibilitychange')) + }, state) + } + + clearReceivedSpans() { + this.receivedSpans = [] + } + + async flushData() { + await this.page.evaluate(() => { + if (window.SplunkRum) { + window.SplunkRum._processor.forceFlush() + } + }) + } + + async getCookie(name: string) { + const cookies = await this.context.cookies() + return cookies.find((cookie) => cookie.name === name) + } + + goTo(url: string) { + const absoluteUrl = new URL(url, 'http://localhost:3000').toString() + return this.page.goto(absoluteUrl) + } + + get locator() { + return this.page.locator.bind(this.page) + } + + get keyboard() { + return this.page.keyboard + } + + get evaluate() { + return this.page.evaluate.bind(this.page) + } + + async mockNetwork() { + await this.page.route('*/**/api/v2/spans', async (route, request) => { + const spans = JSON.parse(request.postData()) + this.receivedSpans.push(...spans) + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: '', + }) + }) + } + + setOffline() { + return this.context.setOffline(true) + } + + setOnline() { + return this.context.setOffline(false) + } + + waitForSpans: (predicate: (spans: Span[]) => boolean) => Promise = async (predicate) => + new Promise((resolve) => { + const intervalId = setInterval(async () => { + await this.flushData() + if (predicate(this.receivedSpans)) { + clearInterval(intervalId) + resolve() + } + }, 50) + }) + + waitForTestToFinish = async () => { + await this.page.evaluate( + () => + new Promise((resolve) => { + const interval = setInterval(() => { + // TODO: Add d.ts file + // @ts-expect-error testing is a global variable + if (window.testing === false) { + clearInterval(interval) + resolve() + } + }, 50) + }), + ) + } + + waitForTimeout(timeout: number) { + return this.page.waitForTimeout(timeout) + } + + async waitForTimeoutAndFlushData(timeout: number) { + await this.waitForTimeout(timeout) + await this.flushData() + } +} diff --git a/packages/web/integration-tests/tests/redacting-attributes/index.spec.js b/packages/integration-tests/src/server/middlewares/delay-middleware.ts similarity index 58% rename from packages/web/integration-tests/tests/redacting-attributes/index.spec.js rename to packages/integration-tests/src/server/middlewares/delay-middleware.ts index cffd1b40..a5f8edc1 100644 --- a/packages/web/integration-tests/tests/redacting-attributes/index.spec.js +++ b/packages/integration-tests/src/server/middlewares/delay-middleware.ts @@ -15,16 +15,13 @@ * limitations under the License. * */ +import { Request, Response, NextFunction } from 'express' -module.exports = { - 'redacting attributes with custom exporter': async function (browser) { - const url = browser.globals.getUrl('/redacting-attributes/index.ejs') - await browser.url(url) +export const delayMiddleware = async (req: Request, res: Response, next: NextFunction) => { + if (typeof req.query.delay === 'string') { + const timeout = parseInt(req.query.delay) + await new Promise((resolve) => setTimeout(resolve, timeout)) + } - await browser.click('#btnClick') - - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - await browser.assert.ok(clickSpan) - await browser.assert.strictEqual(clickSpan.tags['http.url'], '[redacted]') - }, + next() } diff --git a/packages/integration-tests/src/server/middlewares/index.ts b/packages/integration-tests/src/server/middlewares/index.ts new file mode 100644 index 00000000..fe10c2e4 --- /dev/null +++ b/packages/integration-tests/src/server/middlewares/index.ts @@ -0,0 +1,20 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +export * from './delay-middleware' +export * from './server-timing-middleware' +export * from './no-cache-middleware' diff --git a/packages/web/integration-tests/tests/native/native.spec.js b/packages/integration-tests/src/server/middlewares/no-cache-middleware.ts similarity index 56% rename from packages/web/integration-tests/tests/native/native.spec.js rename to packages/integration-tests/src/server/middlewares/no-cache-middleware.ts index f82c74d9..50aa9aeb 100644 --- a/packages/web/integration-tests/tests/native/native.spec.js +++ b/packages/integration-tests/src/server/middlewares/no-cache-middleware.ts @@ -15,17 +15,17 @@ * limitations under the License. * */ +import { Request, Response, NextFunction } from 'express' +import onHeaders from 'on-headers' -module.exports = { - 'native session id integration': async function (browser) { - const url = browser.globals.getUrl('/native/native.ejs') - await browser.url(url) +export const noCacheMiddleware = (req: Request, res: Response, next: NextFunction) => { + if (typeof req.query.noCache === 'string') { + onHeaders(res, () => { + res.setHeader('Cache-Control', 'no-store, max-age=0') + res.removeHeader('Etag') + res.removeHeader('Last-Modified') + }) + } - const anySpan = await browser.globals.findSpan((span) => span.tags['splunk.rumSessionId'] !== undefined) - - await browser.assert.ok(anySpan) - - await browser.assert.strictEqual(anySpan.tags['splunk.rumSessionId'], '12341234123412341234123412341234') - await browser.globals.assertNoErrorSpans() - }, + next() } diff --git a/packages/web/integration-tests/tests/xhr/xhr-ignored.spec.js b/packages/integration-tests/src/server/middlewares/server-timing-middleware.ts similarity index 54% rename from packages/web/integration-tests/tests/xhr/xhr-ignored.spec.js rename to packages/integration-tests/src/server/middlewares/server-timing-middleware.ts index 336f9de5..0f24508c 100644 --- a/packages/web/integration-tests/tests/xhr/xhr-ignored.spec.js +++ b/packages/integration-tests/src/server/middlewares/server-timing-middleware.ts @@ -15,18 +15,15 @@ * limitations under the License. * */ +import { NextFunction, Request, Response } from 'express' +import { setServerTimingHeader } from '../server-timing' -module.exports = { - 'XHR request can be ignored': async function (browser) { - await browser.url(browser.globals.getUrl('/xhr/views/xhr-ignored.ejs')) +export const serverTimingMiddleware = (req: Request, res: Response, next: NextFunction) => { + if (typeof req.query.serverTiming === 'string') { + res.setHeader('Server-Timing', req.query.serverTiming) + } else { + setServerTimingHeader(res) + } - await browser.globals.findSpan((span) => span.name === 'guard-span') - - await browser.assert.not.ok( - browser.globals.getReceivedSpans().find((span) => span.tags['http.url'] === '/some-data'), - ) - await browser.assert.not.ok( - browser.globals.getReceivedSpans().find((span) => span.tags['http.url'] === '/no-server-timings'), - ) - }, + next() } diff --git a/packages/integration-tests/src/server/render-agent.ts b/packages/integration-tests/src/server/render-agent.ts new file mode 100644 index 00000000..953af2dc --- /dev/null +++ b/packages/integration-tests/src/server/render-agent.ts @@ -0,0 +1,73 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +export const RENDER_AGENT_TEMPLATE = ` + + + +` diff --git a/packages/integration-tests/src/server/server-timing.ts b/packages/integration-tests/src/server/server-timing.ts new file mode 100644 index 00000000..4a7bf3b6 --- /dev/null +++ b/packages/integration-tests/src/server/server-timing.ts @@ -0,0 +1,36 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { Response } from 'express' + +const generateHex = (length: number) => + [...Array(length).keys()].map(() => Math.floor(Math.random() * 16).toString(16)).join('') + +export const generateServerTiming = () => { + const traceId = generateHex(32) + const spanId = generateHex(16) + const formatted = `traceparent;desc="00-${traceId}-${spanId}-01"` + return { + traceId, + spanId, + header: formatted, + } +} + +export const setServerTimingHeader = (responseObject: Response, serverTiming = generateServerTiming()) => { + responseObject.set('Server-Timing', serverTiming.header) +} diff --git a/packages/integration-tests/src/server/server.ts b/packages/integration-tests/src/server/server.ts new file mode 100644 index 00000000..beafd693 --- /dev/null +++ b/packages/integration-tests/src/server/server.ts @@ -0,0 +1,122 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import express from 'express' +import path from 'path' +import { RENDER_AGENT_TEMPLATE } from './render-agent' +import { render } from 'ejs' +import * as fs from 'node:fs' +import cors from 'cors' +import { serverTimingMiddleware, delayMiddleware, noCacheMiddleware } from './middlewares' + +const GLOBAL_TEST_BUFFER_TIMEOUT = 20 + +const app = express() + +app.set('view engine', 'ejs') +app.set('views', path.join(__dirname, '../tests')) + +app.use(cors()) +app.use(express.json()) +app.use(express.static(path.join(__dirname, '../../../web/dist'))) +app.use(delayMiddleware) +app.use(serverTimingMiddleware) +app.use(noCacheMiddleware) + +app.post('/api/v2/spans', (req, res) => { + res.send('') +}) + +app.post('/echo', (req, res) => { + res.send(req.body) +}) + +app.get('/some-data', (req, response) => { + response.send({ + key1: 'value1', + key2: 'value2', + key3: 'value3', + }) +}) + +app.get('/no-server-timings', (_, res) => { + res.removeHeader('Server-Timing') + res.sendStatus(200) +}) + +app.get('/', (_, res) => { + res.sendStatus(200) +}) + +app.get('*', (req, res) => { + const beaconUrl = new URL(`http://${req.headers.host}/api/v2/spans`) + const defaultFile = '/artifacts/splunk-otel-web.js' + + const filePath = path.join(__dirname, '../tests', req.path) + if (fs.existsSync(filePath)) { + if (req.path.endsWith('.ejs')) { + res.render(req.path.slice(1), { + renderAgent(userOpts = {}, noInit = false, file = defaultFile, cdnVersion = null) { + const options: Record = { + beaconEndpoint: beaconUrl.toString(), + applicationName: 'splunk-otel-js-dummy-app', + debug: true, + bufferTimeout: GLOBAL_TEST_BUFFER_TIMEOUT, + ...userOpts, + } + + if (typeof req.query.disableInstrumentation === 'string') { + if (!options.instrumentations) { + options.instrumentations = {} + } + + req.query.disableInstrumentation.split(',').forEach((instrumentation) => { + options.instrumentations[instrumentation] = false + }) + } + + if (typeof req.query.beaconEndpoint === 'string') { + options.beaconEndpoint = req.query.beaconEndpoint + } + + if (cdnVersion) { + file = `https://cdn.signalfx.com/o11y-gdi-rum/${cdnVersion}/splunk-otel-web.js` + } + + return render(RENDER_AGENT_TEMPLATE, { + file, + noInit, + options: JSON.stringify(options), + otelApiGlobalsFile: '/artifacts/otel-api-globals.js', + }) + }, + }) + } else { + res.sendFile(filePath) + } + } else { + res.status(404).send('Error: Invalid URL') + } +}) + +app.listen(3000, () => { + console.log(`Server running on port 3000`) +}) + +app.listen(3001, () => { + console.log(`Server running on port 3001`) +}) diff --git a/packages/web/integration-tests/tests/cdn/index.ejs b/packages/integration-tests/src/tests/cdn/index.ejs similarity index 87% rename from packages/web/integration-tests/tests/cdn/index.ejs rename to packages/integration-tests/src/tests/cdn/index.ejs index 44b8027f..c16fa380 100644 --- a/packages/web/integration-tests/tests/cdn/index.ejs +++ b/packages/integration-tests/src/tests/cdn/index.ejs @@ -7,7 +7,7 @@ <%- renderAgent({}, false, null, 'latest' ) %> diff --git a/packages/integration-tests/src/tests/cdn/index.spec.ts b/packages/integration-tests/src/tests/cdn/index.spec.ts new file mode 100644 index 00000000..2da4624a --- /dev/null +++ b/packages/integration-tests/src/tests/cdn/index.spec.ts @@ -0,0 +1,61 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { expect } from '@playwright/test' +import { test } from '../../utils/test' + +test.describe('cdn', () => { + test('JS unhandled error', async ({ browserName, recordPage }) => { + await recordPage.goTo('/cdn/index.ejs') + + await recordPage.waitForSpans((spans) => spans.some((s) => s.name === 'onerror')) + const errorSpans = recordPage.receivedSpans.filter((s) => s.name === 'onerror') + + expect(errorSpans).toHaveLength(1) + + const errorSpan = errorSpans[0] + expect(errorSpan.tags['component']).toBe('error') + expect(errorSpan.tags['error']).toBe('true') + expect(errorSpan.tags['error.object']).toBe('TypeError') + + let expectedMessage = '' + + switch (browserName) { + case 'chromium': + expectedMessage = "Cannot set properties of null (setting 'prop1')" + break + case 'firefox': + expectedMessage = 'test is null' + break + case 'webkit': + expectedMessage = "null is not an object (evaluating 'test.prop1 = true')" + break + } + + expect(errorSpan.tags['error.message']).toBe(expectedMessage) + + const rumScriptFetchSpans = recordPage.receivedSpans.filter( + (s) => s.name === 'resourceFetch' && (s.tags['http.url']).includes('cdn.signalfx.com'), + ) + expect(rumScriptFetchSpans).toHaveLength(1) + const rumScriptFetchSpan = rumScriptFetchSpans[0] + expect(rumScriptFetchSpan.tags['http.url']).toBe( + 'https://cdn.signalfx.com/o11y-gdi-rum/latest/splunk-otel-web.js', + ) + expect(rumScriptFetchSpan.tags['splunk.rumVersion']).toBe('0.19.3') + }) +}) diff --git a/packages/web/integration-tests/tests/connectivity/connectivity.ejs b/packages/integration-tests/src/tests/connectivity/connectivity.ejs similarity index 100% rename from packages/web/integration-tests/tests/connectivity/connectivity.ejs rename to packages/integration-tests/src/tests/connectivity/connectivity.ejs diff --git a/packages/integration-tests/src/tests/connectivity/connectivity.spec.ts b/packages/integration-tests/src/tests/connectivity/connectivity.spec.ts new file mode 100644 index 00000000..920ae309 --- /dev/null +++ b/packages/integration-tests/src/tests/connectivity/connectivity.spec.ts @@ -0,0 +1,66 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { expect } from '@playwright/test' +import { test } from '../../utils/test' + +test.describe('connectivity', () => { + test('Connectivity events are captured', async ({ browserName, recordPage }) => { + if (browserName === 'firefox') { + // TODO: Investigate why setOffline is not working in firefox + test.skip() + } + + await recordPage.goTo('/connectivity/connectivity.ejs') + + await recordPage.waitForSpans((spans) => spans.some((s) => s.name === 'documentFetch')) + + // Wait some time to see if we get any connectivity spans + await recordPage.waitForTimeoutAndFlushData(1000) + const connectivitySpansBeforeOffline = recordPage.receivedSpans.filter((span) => span.name === 'connectivity') + + expect(connectivitySpansBeforeOffline.length).toBe(0) + + await recordPage.setOffline() + + await recordPage.waitForTimeoutAndFlushData(1000) + const connectivitySpansAfterOffline = recordPage.receivedSpans.filter((span) => span.name === 'connectivity') + + expect(connectivitySpansAfterOffline.length).toBe(0) + + await recordPage.setOnline() + await recordPage.waitForSpans((spans) => spans.filter((s) => s.name === 'connectivity').length >= 1) + const connectivitySpansAfterOnline = recordPage.receivedSpans.filter((span) => span.name === 'connectivity') + + expect(connectivitySpansAfterOnline).toHaveLength(2) + expect(connectivitySpansAfterOnline[0].tags['online']).toBe('false') + expect(connectivitySpansAfterOnline[1].tags['online']).toBe('true') + + expect(recordPage.receivedErrorSpans).toHaveLength(0) + + await recordPage.setOffline() + await recordPage.setOnline() + + await recordPage.waitForSpans((spans) => spans.filter((s) => s.name === 'connectivity').length >= 4) + const allConnectivitySpans = recordPage.receivedSpans.filter((span) => span.name === 'connectivity') + + expect(allConnectivitySpans).toHaveLength(4) + expect(allConnectivitySpans[2].tags['online']).toBe('false') + expect(allConnectivitySpans[3].tags['online']).toBe('true') + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) +}) diff --git a/packages/integration-tests/src/tests/context/async-context.spec.ts b/packages/integration-tests/src/tests/context/async-context.spec.ts new file mode 100644 index 00000000..51211a1b --- /dev/null +++ b/packages/integration-tests/src/tests/context/async-context.spec.ts @@ -0,0 +1,128 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from '../../utils/test' +import { expect } from '@playwright/test' +import { RecordPage } from '../../pages/record-page' + +const runTest = async (recordPage: RecordPage, urlPath: string) => { + await recordPage.goTo(urlPath) + + await recordPage.locator('#btn1').click() + + await recordPage.waitForTestToFinish() + + const clickSpans = recordPage.receivedSpans.filter((span) => span.name === 'click') + const childSpans = recordPage.receivedSpans.filter((span) => span.name === 'context-child') + + expect(clickSpans).toHaveLength(1) + expect(clickSpans[0].parentId).toBeUndefined() + expect(childSpans).toHaveLength(1) + expect(childSpans[0].parentId).toBe(clickSpans[0].id) + + expect(recordPage.receivedErrorSpans).toHaveLength(0) +} + +test.describe('context', () => { + test('setTimeout', async ({ recordPage }) => { + await runTest(recordPage, '/context/set-timeout.ejs') + recordPage.clearReceivedSpans() + + await recordPage.locator('#btn2').click() + await recordPage.waitForTimeout(1000) + await recordPage.waitForTestToFinish() + + const clickSpans = recordPage.receivedSpans.filter((span) => span.name === 'click') + const childSpans = recordPage.receivedSpans.filter((span) => span.name === 'context-child') + + expect(clickSpans).toHaveLength(1) + expect(childSpans).toHaveLength(1) + expect(childSpans[0].parentId, "Child span in a longer timeout doesn't have a parent").toBeUndefined() + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) + + test('promise constructor', async ({ recordPage }) => { + await recordPage.goTo('/context/promise.ejs') + + for (let i = 1; i <= 5; i++) { + await recordPage.locator('#btn' + i).click() + await recordPage.waitForTimeout(1000) + await recordPage.waitForTestToFinish() + + const clickSpans = recordPage.receivedSpans.filter((span) => span.name === 'click') + const childSpans = recordPage.receivedSpans.filter((span) => span.name === 'context-child') + + expect(clickSpans).toHaveLength(1) + expect(childSpans).toHaveLength(1) + expect(childSpans[0].parentId).toBeDefined() + expect(childSpans[0].parentId, `Child span ${i} belongs to user interaction trace.`).toBe(clickSpans[0].id) + + expect(recordPage.receivedErrorSpans).toHaveLength(0) + recordPage.clearReceivedSpans() + } + }) + + test('fetch then', async ({ recordPage }) => { + await runTest(recordPage, '/context/fetch-then.ejs') + }) + + test('xhr event callback', async ({ recordPage }) => { + await runTest(recordPage, '/context/xhr-events.ejs') + }) + + test('xhr event with removal', async ({ recordPage }) => { + await runTest(recordPage, '/context/xhr-events-removed.ejs') + }) + + test('xhr onload property', async ({ recordPage }) => { + await runTest(recordPage, '/context/xhr-onload.ejs') + }) + + test('mutation observer on textNode', async ({ recordPage }) => { + await runTest(recordPage, '/context/mutation-observer-textnode.ejs') + }) + + test('MessagePort - addEventListener', async ({ recordPage }) => { + await runTest(recordPage, '/context/messageport-addeventlistener.ejs') + }) + + test('MessagePort - onmessage', async ({ recordPage }) => { + await runTest(recordPage, '/context/messageport-onmessage.ejs') + }) + + test('MessagePort: Works with cors', async ({ recordPage }) => { + await recordPage.goTo('/context/messageport-cors.ejs') + await recordPage.waitForTestToFinish() + + const messageEventSpans = recordPage.receivedSpans.filter((span) => span.name === 'message-event') + + expect(messageEventSpans).toHaveLength(1) + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) + + test('location - hashchange', async ({ recordPage }) => { + await runTest(recordPage, '/context/location-hash.ejs') + + const clickSpans = recordPage.receivedSpans.filter((span) => span.name === 'click') + const routeChangeSpans = recordPage.receivedSpans.filter((span) => span.name === 'routeChange') + + expect(clickSpans).toHaveLength(1) + expect(routeChangeSpans).toHaveLength(1) + + expect(routeChangeSpans[0].parentId).toBe(clickSpans[0].id) + }) +}) diff --git a/packages/web/integration-tests/tests/context/fetch-then.ejs b/packages/integration-tests/src/tests/context/fetch-then.ejs similarity index 100% rename from packages/web/integration-tests/tests/context/fetch-then.ejs rename to packages/integration-tests/src/tests/context/fetch-then.ejs diff --git a/packages/integration-tests/src/tests/context/framework/framework.spec.ts b/packages/integration-tests/src/tests/context/framework/framework.spec.ts new file mode 100644 index 00000000..d099e287 --- /dev/null +++ b/packages/integration-tests/src/tests/context/framework/framework.spec.ts @@ -0,0 +1,74 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from '../../../utils/test' +import { expect } from '@playwright/test' +import { RecordPage } from '../../../pages/record-page' + +const runTest = async (recordPage: RecordPage, urlPath: string) => { + recordPage.clearReceivedSpans() + + await recordPage.goTo(urlPath) + await recordPage.locator('#btn1').click() + + await recordPage.waitForTestToFinish() + + const clickSpans = recordPage.receivedSpans.filter((span) => span.name === 'click') + const customSpans = recordPage.receivedSpans.filter((span) => span.name === 'custom-span') + + expect(clickSpans).toHaveLength(1) + expect(customSpans).toHaveLength(1) + + expect(clickSpans[0].parentId).toBeUndefined() + expect(customSpans[0].parentId).toBe(clickSpans[0].id) + expect(customSpans[0].traceId).toBe(clickSpans[0].traceId) + + expect(recordPage.receivedErrorSpans).toHaveLength(0) +} + +test.describe('framework', () => { + test('Vue 2 with async', async ({ recordPage }) => { + await runTest(recordPage, '/context/framework/vue2.ejs') + }) + + test('Vue 3 with async', async ({ recordPage }) => { + await runTest(recordPage, '/context/framework/vue3.ejs') + }) + + test('React 16 with async', async ({ recordPage }) => { + await runTest(recordPage, '/context/framework/react-16.ejs') + }) + + test('React latest with async', async ({ recordPage }) => { + await recordPage.goTo('/context/framework/react-latest.ejs') + await recordPage.locator('#btn1').click() + await recordPage.waitForTestToFinish() + + const clickSpans = recordPage.receivedSpans.filter((span) => span.name === 'click') + const customSpans = recordPage.receivedSpans.filter((span) => span.name === 'custom-span') + + expect(clickSpans.length).toBeGreaterThanOrEqual(1) + expect(customSpans).toHaveLength(1) + + // TODO: Investigate for some reason for react 17 there are several nested clicks + clickSpans.forEach((span) => { + expect(customSpans[0].traceId, span.traceId) + }) + + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) +}) diff --git a/packages/web/integration-tests/tests/context/framework/react-16.ejs b/packages/integration-tests/src/tests/context/framework/react-16.ejs similarity index 100% rename from packages/web/integration-tests/tests/context/framework/react-16.ejs rename to packages/integration-tests/src/tests/context/framework/react-16.ejs diff --git a/packages/web/integration-tests/tests/context/framework/react-latest.ejs b/packages/integration-tests/src/tests/context/framework/react-latest.ejs similarity index 100% rename from packages/web/integration-tests/tests/context/framework/react-latest.ejs rename to packages/integration-tests/src/tests/context/framework/react-latest.ejs diff --git a/packages/web/integration-tests/tests/context/framework/vue2.ejs b/packages/integration-tests/src/tests/context/framework/vue2.ejs similarity index 92% rename from packages/web/integration-tests/tests/context/framework/vue2.ejs rename to packages/integration-tests/src/tests/context/framework/vue2.ejs index ac6ded46..4564484b 100644 --- a/packages/web/integration-tests/tests/context/framework/vue2.ejs +++ b/packages/integration-tests/src/tests/context/framework/vue2.ejs @@ -15,7 +15,7 @@

Vue 2

-
+
@@ -27,7 +27,7 @@ } }) - var app = new Vue({ + const app = new Vue({ el: '#app', data: { toggle: false @@ -35,4 +35,4 @@ }) - \ No newline at end of file + diff --git a/packages/web/integration-tests/tests/context/framework/vue3.ejs b/packages/integration-tests/src/tests/context/framework/vue3.ejs similarity index 100% rename from packages/web/integration-tests/tests/context/framework/vue3.ejs rename to packages/integration-tests/src/tests/context/framework/vue3.ejs diff --git a/packages/web/integration-tests/tests/context/location-hash.ejs b/packages/integration-tests/src/tests/context/location-hash.ejs similarity index 100% rename from packages/web/integration-tests/tests/context/location-hash.ejs rename to packages/integration-tests/src/tests/context/location-hash.ejs diff --git a/packages/web/integration-tests/tests/context/messageport-addeventlistener.ejs b/packages/integration-tests/src/tests/context/messageport-addeventlistener.ejs similarity index 95% rename from packages/web/integration-tests/tests/context/messageport-addeventlistener.ejs rename to packages/integration-tests/src/tests/context/messageport-addeventlistener.ejs index 90b19eef..2af397d4 100644 --- a/packages/web/integration-tests/tests/context/messageport-addeventlistener.ejs +++ b/packages/integration-tests/src/tests/context/messageport-addeventlistener.ejs @@ -16,7 +16,7 @@ - - + + diff --git a/packages/web/integration-tests/tests/docload/docload-ignored.ejs b/packages/integration-tests/src/tests/docload/docload-ignored.ejs similarity index 100% rename from packages/web/integration-tests/tests/docload/docload-ignored.ejs rename to packages/integration-tests/src/tests/docload/docload-ignored.ejs diff --git a/packages/web/integration-tests/tests/docload/docload.ejs b/packages/integration-tests/src/tests/docload/docload.ejs similarity index 100% rename from packages/web/integration-tests/tests/docload/docload.ejs rename to packages/integration-tests/src/tests/docload/docload.ejs diff --git a/packages/integration-tests/src/tests/docload/docload.spec.ts b/packages/integration-tests/src/tests/docload/docload.spec.ts new file mode 100644 index 00000000..f6dfa48f --- /dev/null +++ b/packages/integration-tests/src/tests/docload/docload.spec.ts @@ -0,0 +1,137 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { expect } from '@playwright/test' +import { test } from '../../utils/test' +import { timesMakeSense } from '../../utils/time-make-sense' + +test.describe('docload', () => { + test('resources before load event are correctly captured', async ({ recordPage }) => { + await recordPage.goTo('/docload/docload-all.ejs') + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'documentLoad').length === 1) + const docLoadSpans = recordPage.receivedSpans.filter((span) => span.name === 'documentLoad') + + expect(docLoadSpans).toHaveLength(1) + + const resources = ['css-font-img.css', 'splunk-black.png?delay=300', 'iframe.ejs', 'splunk.woff'] + for (const urlEnd of resources) { + const resourceSpans = recordPage.receivedSpans.filter( + (span) => span.tags['http.url'] && (span.tags['http.url'] as string).endsWith(urlEnd), + ) + + expect(docLoadSpans[0].traceId, `${urlEnd} has correct traceId`).toBe(resourceSpans[0].traceId) + } + + const ignoredResources = ['/some-data', '/some-data?delay=1', '/api/v2/spans'] + for (const urlEnd of ignoredResources) { + const resourceSpans = recordPage.receivedSpans.filter( + (span) => + span.tags['component'] === 'document-load' && + typeof span.tags['http.url'] === 'string' && + span.tags['http.url'].endsWith(urlEnd), + ) + + expect(resourceSpans, `${urlEnd} is not captured`).toHaveLength(0) + } + }) + + test('documentFetch, resourceFetch, and documentLoad spans', async ({ recordPage }) => { + await recordPage.goTo('/docload/docload.ejs') + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'documentLoad').length === 1) + const docLoadSpans = recordPage.receivedSpans.filter((span) => span.name === 'documentLoad') + const docFetchSpans = recordPage.receivedSpans.filter((span) => span.name === 'documentFetch') + const scriptFetchSpans = recordPage.receivedSpans.filter( + (span) => + span.name === 'resourceFetch' && + typeof span.tags['http.url'] === 'string' && + span.tags['http.url'].includes('splunk-otel-web.js'), + ) + const brokenImageFetchSpans = recordPage.receivedSpans.filter( + (span) => + span.name === 'resourceFetch' && + typeof span.tags['http.url'] === 'string' && + span.tags['http.url'].includes('/nosuchimage.jpg'), + ) + + expect(docFetchSpans).toHaveLength(1) + expect(docLoadSpans).toHaveLength(1) + expect(docLoadSpans[0].traceId.match(/[a-f0-9]+/), 'Checking sanity of traceId').toBeTruthy() + expect(docLoadSpans[0].id.match(/[a-f0-9]+/), 'Checking sanity of id').toBeTruthy() + expect(docFetchSpans[0].traceId).toBe(docLoadSpans[0].traceId) + expect(docFetchSpans[0].parentId).toBe(docLoadSpans[0].id) + + expect(scriptFetchSpans).toHaveLength(1) + expect(scriptFetchSpans[0].traceId).toBe(docLoadSpans[0].traceId) + expect(scriptFetchSpans[0].parentId).toBe(docLoadSpans[0].id) + expect(scriptFetchSpans[0].tags['component']).toBe('document-load') + + expect(brokenImageFetchSpans.length).toBeGreaterThanOrEqual(1) + expect(brokenImageFetchSpans[0].traceId).toBe(docLoadSpans[0].traceId) + expect(brokenImageFetchSpans[0].parentId).toBe(docLoadSpans[0].id) + + expect(docFetchSpans[0].tags['component']).toBe('document-load') + expect(docLoadSpans[0].tags['location.href']).toBe('http://localhost:3000/docload/docload.ejs') + + timesMakeSense(docFetchSpans[0].annotations, 'domainLookupStart', 'domainLookupEnd') + timesMakeSense(docFetchSpans[0].annotations, 'connectStart', 'connectEnd') + timesMakeSense(docFetchSpans[0].annotations, 'requestStart', 'responseStart') + timesMakeSense(docFetchSpans[0].annotations, 'responseStart', 'responseEnd') + timesMakeSense(docFetchSpans[0].annotations, 'fetchStart', 'responseEnd') + + expect(docFetchSpans[0].tags['link.traceId']).toBeDefined() + expect(docFetchSpans[0].tags['link.spanId']).toBeDefined() + expect(parseInt(scriptFetchSpans[0].tags['http.response_content_length'] as string)).toBeGreaterThan(0) + + expect(docLoadSpans[0].tags['component']).toBe('document-load') + expect(docLoadSpans[0].tags['location.href']).toBe('http://localhost:3000/docload/docload.ejs') + expect( + (docLoadSpans[0].tags['screen.xy'] as string).match(/[0-9]+x[0-9]+/), + 'Checking sanity of screen.xy', + ).toBeTruthy() + + timesMakeSense(docLoadSpans[0].annotations, 'domContentLoadedEventStart', 'domContentLoadedEventEnd') + timesMakeSense(docLoadSpans[0].annotations, 'loadEventStart', 'loadEventEnd') + timesMakeSense(docLoadSpans[0].annotations, 'fetchStart', 'domInteractive') + timesMakeSense(docLoadSpans[0].annotations, 'fetchStart', 'domComplete') + + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) + + test('ignoring resource URLs', async ({ recordPage }) => { + await recordPage.goTo('/docload/docload-ignored.ejs') + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'documentFetch').length === 1) + const ignoredResourceFetchSpans = recordPage.receivedSpans.filter( + (span) => span.tags['http.url'] === 'http://localhost:3000/non-impactful-resource.jpg', + ) + + expect(ignoredResourceFetchSpans).toHaveLength(0) + }) + + test('module can be disabled', async ({ recordPage }) => { + await recordPage.goTo('/docload/docload.ejs?disableInstrumentation=document') + + await recordPage.waitForTimeoutAndFlushData(1000) + const SPAN_TYPES = ['documentFetch', 'documentLoad', 'resourceFetch'] + const documentSpans = recordPage.receivedSpans.filter((span) => SPAN_TYPES.includes(span.name)) + + expect(documentSpans).toHaveLength(0) + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) +}) diff --git a/packages/web/integration-tests/tests/docload/iframe.ejs b/packages/integration-tests/src/tests/docload/iframe.ejs similarity index 100% rename from packages/web/integration-tests/tests/docload/iframe.ejs rename to packages/integration-tests/src/tests/docload/iframe.ejs diff --git a/packages/integration-tests/src/tests/errors/errors.spec.ts b/packages/integration-tests/src/tests/errors/errors.spec.ts new file mode 100644 index 00000000..68e12db0 --- /dev/null +++ b/packages/integration-tests/src/tests/errors/errors.spec.ts @@ -0,0 +1,179 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { expect } from '@playwright/test' +import { test } from '../../utils/test' + +test.describe('errors', () => { + test('DOM resource 4xx', async ({ recordPage }) => { + await recordPage.goTo('/errors/views/resource-4xx.ejs') + await recordPage.waitForSpans( + (spans) => spans.filter((span) => span.name === 'eventListener.error').length === 1, + ) + const errorSpans = recordPage.receivedSpans.filter((span) => span.name === 'eventListener.error') + + expect(errorSpans).toHaveLength(1) + + expect(errorSpans[0].tags['component']).toBe('error') + expect(errorSpans[0].tags['error.type']).toBe('error') + expect(errorSpans[0].tags['target_element']).toBe('IMG') + expect(errorSpans[0].tags['target_xpath']).toBe('//html/body/img') + expect( + (errorSpans[0].tags['target_src'] as string).endsWith('/nonexistent.png'), + `Checking target_src: ${errorSpans[0]['target_src']}`, + ).toBeTruthy() + }) + + test('JS syntax error', async ({ recordPage, browserName }) => { + await recordPage.goTo('/errors/views/js-syntax-error.ejs') + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'onerror').length === 1) + const errorSpans = recordPage.receivedSpans.filter((span) => span.name === 'onerror') + + expect(errorSpans).toHaveLength(1) + + expect(errorSpans[0].tags['component']).toBe('error') + expect(errorSpans[0].tags['error']).toBe('true') + expect(errorSpans[0].tags['error.object']).toBe('SyntaxError') + + const errorMessageMap = { + chromium: "Unexpected token ';'", + firefox: "expected expression, got ';'", + webkit: "Unexpected token ';'", + } + + expect(errorSpans[0].tags['error.message']).toBe(errorMessageMap[browserName]) + }) + + test('JS unhandled error', async ({ recordPage, browserName }) => { + await recordPage.goTo('/errors/views/unhandled-error.ejs') + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'onerror').length === 1) + const errorSpans = recordPage.receivedSpans.filter((span) => span.name === 'onerror') + + expect(errorSpans).toHaveLength(1) + + expect(errorSpans[0].tags['component']).toBe('error') + expect(errorSpans[0].tags['error']).toBe('true') + expect(errorSpans[0].tags['error.object']).toBe('TypeError') + + const errorMessageMap = { + chromium: "Cannot set properties of null (setting 'prop1')", + firefox: 'test is null', + webkit: "null is not an object (evaluating 'test.prop1 = true')", + } + + expect(errorSpans[0].tags['error.message']).toBe(errorMessageMap[browserName]) + }) + + test('unhandled promise rejection', async ({ recordPage }) => { + await recordPage.goTo('/errors/views/unhandled-rejection.ejs') + await recordPage.waitForSpans( + (spans) => spans.filter((span) => span.name === 'unhandledrejection').length === 1, + ) + const errorSpans = recordPage.receivedSpans.filter((span) => span.name === 'unhandledrejection') + + expect(errorSpans).toHaveLength(1) + expect(errorSpans[0].tags['component']).toBe('error') + expect(errorSpans[0].tags['error']).toBe('true') + expect(errorSpans[0].tags['error.object']).toBe('String') + expect(errorSpans[0].tags['error.message']).toBe('rejection-value') + }) + + test('manual console.error', async ({ recordPage, browserName }) => { + await recordPage.goTo('/errors/views/console-error.ejs') + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'console.error').length === 1) + const errorSpans = recordPage.receivedSpans.filter((span) => span.name === 'console.error') + + expect(errorSpans).toHaveLength(1) + expect(errorSpans[0].tags['component']).toBe('error') + expect(errorSpans[0].tags['error']).toBe('true') + expect(errorSpans[0].tags['error.object']).toBe('TypeError') + + const errorMessageMap = { + chromium: "Cannot set properties of null (setting 'anyField')", + firefox: 'someNull is null', + webkit: "null is not an object (evaluating 'someNull.anyField = 'value'')", + } + + expect(errorSpans[0].tags['error.message']).toBe(errorMessageMap[browserName]) + }) + + test('SplunkRum.error', async ({ recordPage, browserName }) => { + const url = 'http://localhost:3000/errors/views/splunkrum-error.ejs' + await recordPage.goTo(url) + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'SplunkRum.error').length === 1) + const errorSpans = recordPage.receivedSpans.filter((span) => span.name === 'SplunkRum.error') + + expect(errorSpans).toHaveLength(1) + expect(errorSpans[0].tags['component']).toBe('error') + expect(errorSpans[0].tags['error']).toBe('true') + expect(errorSpans[0].tags['error.object']).toBe('TypeError') + + const errorMessages = { + chromium: "Cannot set properties of null (setting 'anyField')", + firefox: 'someNull is null', + webkit: "null is not an object (evaluating 'someNull.anyField = 'value'')", + } + + expect(errorSpans[0].tags['error.message']).toBe(errorMessages[browserName]) + + const errorStackMap = { + chromium: `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:63:25`, + firefox: `@${url}:63:7\n`, + webkit: `global code@${url}:63:15`, + } + + expect(errorSpans[0].tags['error.stack']).toBe(errorStackMap[browserName]) + }) + + test('module can be disabled', async ({ recordPage }) => { + await recordPage.goTo('/errors/views/unhandled-error.ejs?disableInstrumentation=errors') + await recordPage.waitForTimeoutAndFlushData(1000) + const errorSpans = recordPage.receivedSpans.filter((span) => span.name === 'onerror') + + expect(errorSpans).toHaveLength(0) + }) + + test('minified script with source map id', async ({ recordPage }) => { + await recordPage.goTo('/errors/views/minified-file-errors.ejs') + await recordPage.locator('#button1').click() + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'onerror').length === 1) + const errorSpans = recordPage.receivedSpans.filter((span) => span.name === 'onerror') + + expect(errorSpans).toHaveLength(1) + expect(errorSpans[0].tags['error.message']).toBe('Error from script1.js') + expect((errorSpans[0].tags['error.source_map_ids'] as string).includes('script1.min.js')).toBeTruthy() + expect( + (errorSpans[0].tags['error.source_map_ids'] as string).includes( + '9663c60664c425cef3b141c167e9b324240ce10ae488726293892b7130266a6b', + ), + ).toBeTruthy() + + await recordPage.locator('#button2').click() + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'onerror').length === 2) + const errorSpans2 = recordPage.receivedSpans.filter((span) => span.name === 'onerror') + + expect(errorSpans2).toHaveLength(2) + expect(errorSpans2[1].tags['error.message']).toBe('Error from script2.js') + expect((errorSpans2[1].tags['error.source_map_ids'] as string).includes('script2.min.js')).toBeTruthy() + expect( + (errorSpans2[1].tags['error.source_map_ids'] as string).includes( + '9793573cdc2ab308a0b1996bea14253ec8832bc036210475ded0813cafa27066', + ), + ).toBeTruthy() + }) +}) diff --git a/packages/web/integration-tests/tests/errors/views/console-error.ejs b/packages/integration-tests/src/tests/errors/views/console-error.ejs similarity index 93% rename from packages/web/integration-tests/tests/errors/views/console-error.ejs rename to packages/integration-tests/src/tests/errors/views/console-error.ejs index babfa642..e193b851 100644 --- a/packages/web/integration-tests/tests/errors/views/console-error.ejs +++ b/packages/integration-tests/src/tests/errors/views/console-error.ejs @@ -5,7 +5,7 @@ Errors test <%- renderAgent({}) %> - + + diff --git a/packages/web/integration-tests/tests/errors/views/resource-4xx.ejs b/packages/integration-tests/src/tests/errors/views/resource-4xx.ejs similarity index 78% rename from packages/web/integration-tests/tests/errors/views/resource-4xx.ejs rename to packages/integration-tests/src/tests/errors/views/resource-4xx.ejs index ff695a76..53968579 100644 --- a/packages/web/integration-tests/tests/errors/views/resource-4xx.ejs +++ b/packages/integration-tests/src/tests/errors/views/resource-4xx.ejs @@ -3,7 +3,7 @@ Errors test - <%- renderAgent({allowInsecureBeacon: true}) %> + <%- renderAgent({ allowInsecureBeacon: true }) %>

Dom parsing error simulation

diff --git a/packages/web/integration-tests/tests/errors/views/script1.min.js b/packages/integration-tests/src/tests/errors/views/script1.min.js similarity index 100% rename from packages/web/integration-tests/tests/errors/views/script1.min.js rename to packages/integration-tests/src/tests/errors/views/script1.min.js diff --git a/packages/web/integration-tests/tests/errors/views/script2.min.js b/packages/integration-tests/src/tests/errors/views/script2.min.js similarity index 100% rename from packages/web/integration-tests/tests/errors/views/script2.min.js rename to packages/integration-tests/src/tests/errors/views/script2.min.js diff --git a/packages/web/integration-tests/tests/errors/views/splunkrum-error.ejs b/packages/integration-tests/src/tests/errors/views/splunkrum-error.ejs similarity index 94% rename from packages/web/integration-tests/tests/errors/views/splunkrum-error.ejs rename to packages/integration-tests/src/tests/errors/views/splunkrum-error.ejs index 1fbb0035..a3ef1fb9 100644 --- a/packages/web/integration-tests/tests/errors/views/splunkrum-error.ejs +++ b/packages/integration-tests/src/tests/errors/views/splunkrum-error.ejs @@ -5,7 +5,7 @@ Errors test <%- renderAgent({}) %> diff --git a/packages/web/integration-tests/tests/resource-observer/resources-ignored.ejs b/packages/integration-tests/src/tests/resource-observer/resources-ignored.ejs similarity index 67% rename from packages/web/integration-tests/tests/resource-observer/resources-ignored.ejs rename to packages/integration-tests/src/tests/resource-observer/resources-ignored.ejs index a6bb7116..e0477a85 100644 --- a/packages/web/integration-tests/tests/resource-observer/resources-ignored.ejs +++ b/packages/integration-tests/src/tests/resource-observer/resources-ignored.ejs @@ -3,11 +3,11 @@ Resource observer ignore test - + <%- renderAgent({}, true) %> <%- renderAgent() %> diff --git a/packages/integration-tests/src/tests/synthetics/synthetics.spec.ts b/packages/integration-tests/src/tests/synthetics/synthetics.spec.ts new file mode 100644 index 00000000..4925ffb2 --- /dev/null +++ b/packages/integration-tests/src/tests/synthetics/synthetics.spec.ts @@ -0,0 +1,40 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from '../../utils/test' +import { expect } from '@playwright/test' + +test.describe('synthetics', () => { + test('applies synthetics run Id attribute on all spans', async ({ recordPage }) => { + const syntheticsRunId = 'abcd1234'.repeat(4) + await recordPage.goTo('/synthetics/index.ejs?syntheticsRunId=' + syntheticsRunId) + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'guard-span').length === 1) + const receivedSpans = recordPage.receivedSpans + + expect(receivedSpans.every((span) => span.tags['Synthetics-RunId'] === syntheticsRunId)).toBeTruthy() + }) + + test('applies synthetics run id attribute on all spans', async ({ recordPage }) => { + await recordPage.goTo('/synthetics/index.ejs') + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'guard-span').length === 1) + const receivedSpans = recordPage.receivedSpans + + expect(receivedSpans.every((span) => span.tags['Synthetics-RunId'] === undefined)).toBeTruthy() + }) +}) diff --git a/packages/web/integration-tests/tests/user-interaction/causality.ejs b/packages/integration-tests/src/tests/user-interaction/causality.ejs similarity index 100% rename from packages/web/integration-tests/tests/user-interaction/causality.ejs rename to packages/integration-tests/src/tests/user-interaction/causality.ejs diff --git a/packages/integration-tests/src/tests/user-interaction/causality.spec.ts b/packages/integration-tests/src/tests/user-interaction/causality.spec.ts new file mode 100644 index 00000000..b6fcf6d9 --- /dev/null +++ b/packages/integration-tests/src/tests/user-interaction/causality.spec.ts @@ -0,0 +1,60 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from '../../utils/test' +import { expect } from '@playwright/test' + +test.describe('causality', () => { + test('handles causality of a mouse click', async ({ recordPage }) => { + await recordPage.goTo('/user-interaction/causality.ejs') + + await recordPage.locator('body').click() + await recordPage.locator('#btn1').click() + + await recordPage.waitForSpans( + (spans) => + spans.filter((span) => span.name === 'click' && span.tags['target_xpath'] === '//*[@id="btn1"]') + .length === 1 && + spans.filter((span) => span.tags['http.url'] === 'http://localhost:3000/some-data').length === 1, + ) + + const clickSpans = recordPage.receivedSpans.filter( + (span) => span.name === 'click' && span.tags['target_xpath'] === '//*[@id="btn1"]', + ) + const fetchSpans = recordPage.receivedSpans.filter( + (span) => span.tags['http.url'] === 'http://localhost:3000/some-data', + ) + + expect(clickSpans).toHaveLength(1) + expect(fetchSpans).toHaveLength(1) + expect(fetchSpans[0].parentId).toBe(clickSpans[0].id) + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) + + test('can be disabled', async ({ recordPage }) => { + await recordPage.goTo('/user-interaction/causality.ejs?disableInstrumentation=interactions') + + await recordPage.locator('body').click() + await recordPage.locator('#btn1').click() + + await recordPage.waitForTimeoutAndFlushData(1000) + + const clickSpans = recordPage.receivedSpans.filter((span) => span.name === 'click') + + expect(clickSpans).toHaveLength(0) + }) +}) diff --git a/packages/web/integration-tests/tests/user-interaction/forms.ejs b/packages/integration-tests/src/tests/user-interaction/forms.ejs similarity index 100% rename from packages/web/integration-tests/tests/user-interaction/forms.ejs rename to packages/integration-tests/src/tests/user-interaction/forms.ejs diff --git a/packages/integration-tests/src/tests/user-interaction/forms.spec.ts b/packages/integration-tests/src/tests/user-interaction/forms.spec.ts new file mode 100644 index 00000000..36d3ec7c --- /dev/null +++ b/packages/integration-tests/src/tests/user-interaction/forms.spec.ts @@ -0,0 +1,38 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from '../../utils/test' +import { expect } from '@playwright/test' + +test.describe('forms', () => { + test('handles form submit', async ({ recordPage }) => { + await recordPage.goTo('/user-interaction/forms.ejs') + + await recordPage.locator('#btnSubmit').click() + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'submit').length === 1) + + const submitSpans = recordPage.receivedSpans.filter((span) => span.name === 'submit') + + expect(submitSpans).toHaveLength(1) + expect(submitSpans[0].tags['component']).toBe('user-interaction') + expect(submitSpans[0].tags['event_type']).toBe('submit') + expect(submitSpans[0].tags['target_element']).toBe('FORM') + expect(submitSpans[0].tags['target_xpath']).toBe('//*[@id="form1"]') + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) +}) diff --git a/packages/web/integration-tests/tests/user-interaction/history.ejs b/packages/integration-tests/src/tests/user-interaction/history.ejs similarity index 100% rename from packages/web/integration-tests/tests/user-interaction/history.ejs rename to packages/integration-tests/src/tests/user-interaction/history.ejs diff --git a/packages/integration-tests/src/tests/user-interaction/history.spec.ts b/packages/integration-tests/src/tests/user-interaction/history.spec.ts new file mode 100644 index 00000000..50e10754 --- /dev/null +++ b/packages/integration-tests/src/tests/user-interaction/history.spec.ts @@ -0,0 +1,61 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from '../../utils/test' +import { expect } from '@playwright/test' + +test.describe('history', () => { + test('handles hash navigation', async ({ recordPage }) => { + await recordPage.goTo('/user-interaction/history.ejs') + + await recordPage.locator('#btnGoToPage').click() + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'routeChange').length === 1) + const navigationSpans = recordPage.receivedSpans.filter((span) => span.name === 'routeChange') + + expect(navigationSpans).toHaveLength(1) + expect(navigationSpans[0].tags['component']).toBe('user-interaction') + expect(navigationSpans[0].tags['prev.href']).toBe('http://localhost:3000/user-interaction/history.ejs') + expect(navigationSpans[0].tags['location.href']).toBe( + 'http://localhost:3000/user-interaction/history.ejs#another-page', + ) + expect(recordPage.receivedErrorSpans).toHaveLength(0) + }) + + test('handles history navigation', async ({ recordPage }) => { + await recordPage.goTo('/user-interaction/history.ejs') + + await recordPage.locator('#btnGoToPage').click() + await recordPage.locator('#btnGoBack').click() + await recordPage.locator('#btnGoForward').click() + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'routeChange').length === 3) + const navigationSpans = recordPage.receivedSpans.filter((span) => span.name === 'routeChange') + + expect(navigationSpans).toHaveLength(3) + expect(navigationSpans[1].tags['component']).toBe('user-interaction') + expect(navigationSpans[1].tags['prev.href']).toBe( + 'http://localhost:3000/user-interaction/history.ejs#another-page', + ) + expect(navigationSpans[1].tags['location.href']).toBe('http://localhost:3000/user-interaction/history.ejs') + expect(navigationSpans[2].tags['component']).toBe('user-interaction') + expect(navigationSpans[2].tags['prev.href']).toBe('http://localhost:3000/user-interaction/history.ejs') + expect(navigationSpans[2].tags['location.href']).toBe( + 'http://localhost:3000/user-interaction/history.ejs#another-page', + ) + }) +}) diff --git a/packages/web/integration-tests/tests/user-interaction/keyboard.ejs b/packages/integration-tests/src/tests/user-interaction/keyboard.ejs similarity index 96% rename from packages/web/integration-tests/tests/user-interaction/keyboard.ejs rename to packages/integration-tests/src/tests/user-interaction/keyboard.ejs index 2e70f68c..b6f88451 100644 --- a/packages/web/integration-tests/tests/user-interaction/keyboard.ejs +++ b/packages/integration-tests/src/tests/user-interaction/keyboard.ejs @@ -22,6 +22,7 @@ diff --git a/packages/integration-tests/src/tests/xhr/xhr.spec.ts b/packages/integration-tests/src/tests/xhr/xhr.spec.ts new file mode 100644 index 00000000..34c2e0eb --- /dev/null +++ b/packages/integration-tests/src/tests/xhr/xhr.spec.ts @@ -0,0 +1,68 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from '../../utils/test' +import { expect } from '@playwright/test' + +test.describe('xhr', () => { + test('XHR request is registered', async ({ recordPage }) => { + await recordPage.goTo('/xhr/xhr-basic.ejs') + await recordPage.waitForSpans( + (spans) => spans.filter((span) => span.tags['http.url'] === 'http://localhost:3000/some-data').length === 1, + ) + const xhrSpans = recordPage.receivedSpans.filter( + (span) => span.tags['http.url'] === 'http://localhost:3000/some-data', + ) + + expect(xhrSpans).toHaveLength(1) + expect(xhrSpans[0].tags['component']).toBe('xml-http-request') + expect(xhrSpans[0].tags['http.status_code']).toBe('200') + expect(xhrSpans[0].tags['http.status_text']).toBe('OK') + expect(xhrSpans[0].tags['http.method']).toBe('GET') + expect(xhrSpans[0].tags['http.url']).toBe('http://localhost:3000/some-data') + expect(xhrSpans[0].tags['http.response_content_length']).toBe('49') + expect(xhrSpans[0].tags['link.traceId']).toBeTruthy() + expect(xhrSpans[0].tags['link.spanId']).toBeTruthy() + }) + + test('module can be disabled', async ({ recordPage }) => { + await recordPage.goTo('/xhr/xhr-basic.ejs?disableInstrumentation=xhr') + await recordPage.waitForTimeout(1000) + + const xhrSpans = recordPage.receivedSpans.filter( + (span) => span.tags['http.url'] === 'http://localhost:3000/some-data', + ) + + expect(xhrSpans).toHaveLength(0) + }) + + test('XHR request can be ignored', async ({ recordPage }) => { + await recordPage.goTo('/xhr/xhr-ignored.ejs') + + await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'guard-span').length === 1) + + const xhrSpans = recordPage.receivedSpans.filter((span) => + ['http://localhost:3000/some-data', 'http://localhost:3000/no-server-timings'].includes( + span.tags['http.url'] as string, + ), + ) + const guardSpans = recordPage.receivedSpans.filter((span) => span.name === 'guard-span') + + expect(xhrSpans).toHaveLength(0) + expect(guardSpans).toHaveLength(1) + }) +}) diff --git a/packages/integration-tests/src/utils/test.ts b/packages/integration-tests/src/utils/test.ts new file mode 100644 index 00000000..1f6a35c4 --- /dev/null +++ b/packages/integration-tests/src/utils/test.ts @@ -0,0 +1,37 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { test as base } from '@playwright/test' + +import { RecordPage } from '../pages/record-page' + +type TestFixtures = { + recordPage: RecordPage +} + +export const test = base.extend({ + recordPage: async ({ browser }, use) => { + const recordingContext = await browser.newContext({ viewport: { height: 1080, width: 1920 } }) + const recordPage = new RecordPage(await recordingContext.newPage(), recordingContext) + await recordPage.mockNetwork() + + await use(recordPage) + + await recordingContext.close() + }, +}) diff --git a/packages/integration-tests/src/utils/time-make-sense.ts b/packages/integration-tests/src/utils/time-make-sense.ts new file mode 100644 index 00000000..9f5c11d6 --- /dev/null +++ b/packages/integration-tests/src/utils/time-make-sense.ts @@ -0,0 +1,50 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { expect } from '@playwright/test' + +export const timesMakeSense = ( + annotations: Array<{ timestamp: number; value: string }>, + startName: string, + endName: string, +) => { + const annotationsObject: Record = {} + annotations.forEach((event) => { + annotationsObject[event.value] = event.timestamp + }) + + expect(annotationsObject[startName]).toBeTruthy() + expect(annotationsObject[endName]).toBeTruthy() + + expect(annotationsObject[startName]).toBeLessThanOrEqual(annotationsObject[endName]) + + const diff = annotationsObject[endName] - annotationsObject[startName] + const fiveMinutes = 5 * 60 * 1000 * 1000 + + // Sanity check for time difference + expect(diff).toBeLessThanOrEqual(fiveMinutes) + + // Also looking for rough synchronization with reality (at least from our CI systems/laptops...) + const nowMicros = new Date().getTime() * 1000 + let clockSkew = annotationsObject[startName] - nowMicros + if (clockSkew < 0) { + clockSkew = -clockSkew + } + + // Sanity check for clock skew + expect(clockSkew).toBeLessThanOrEqual(fiveMinutes) +} diff --git a/packages/integration-tests/src/version.ts b/packages/integration-tests/src/version.ts new file mode 100644 index 00000000..95513fdf --- /dev/null +++ b/packages/integration-tests/src/version.ts @@ -0,0 +1,20 @@ +/** + * + * Copyright 2024 Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// this is an autogenerated file, see scripts/version-update.js +export const VERSION = '0.19.3' diff --git a/packages/integration-tests/tsconfig.json b/packages/integration-tests/tsconfig.json new file mode 100644 index 00000000..0cd8a3bb --- /dev/null +++ b/packages/integration-tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "include": ["**/*.ts"], + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "target": "esnext", + "moduleResolution": "nodenext" + } +} diff --git a/packages/web/integration-tests/tests/cdn/index.spec.js b/packages/web/integration-tests/tests/cdn/index.spec.js deleted file mode 100644 index 41fbdf30..00000000 --- a/packages/web/integration-tests/tests/cdn/index.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - '@tags': ['skip-ie11'], - 'JS unhandled error': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/cdn/index.ejs')) - - const errorSpan = await browser.globals.findSpan((s) => s.name === 'onerror') - await browser.assert.ok(!!errorSpan, 'Checking presence of error span.') - - const tags = errorSpan.tags - await browser.assert.strictEqual(tags['component'], 'error') - await browser.assert.strictEqual(tags['error'], 'true') - await browser.assert.strictEqual(tags['error.object'], 'TypeError') - - switch (browser.options.desiredCapabilities.browserName.toLowerCase()) { - case 'chrome': - await browser.assert.strictEqual( - tags['error.message'], - "Cannot set properties of null (setting 'prop1')", - ) - break - case 'firefox': - await browser.assert.strictEqual(tags['error.message'], 'test is null') - break - case 'safari': - await browser.assert.strictEqual( - tags['error.message'], - "null is not an object (evaluating 'test.prop1 = true')", - ) - break - } - - const rumScriptFetchSpan = await browser.globals.findSpan( - (s) => s.name === 'resourceFetch' && s.tags['http.url'].includes('cdn.signalfx.com'), - ) - await browser.assert.ok(!!rumScriptFetchSpan, 'Checking presence of splunk-otel-web fetch span.') - - const cdnUrl = 'https://cdn.signalfx.com/o11y-gdi-rum/latest/splunk-otel-web.js' - await browser.assert.strictEqual(rumScriptFetchSpan.tags['http.url'], cdnUrl) - await browser.assert.strictEqual(rumScriptFetchSpan.tags['splunk.rumVersion'], '0.19.3') - }, -} diff --git a/packages/web/integration-tests/tests/connectivity/connectivity.spec.js b/packages/web/integration-tests/tests/connectivity/connectivity.spec.js deleted file mode 100644 index be2e973d..00000000 --- a/packages/web/integration-tests/tests/connectivity/connectivity.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'Connectivity events are captured': async function (browser) { - // When we have a test run that supports chrome dev tools protocol we can properly emulate connectivity. - // Otherwise this could be an unit test. - // Should probably do some timing checks but don't want to add flakiness atm - - await browser.url(browser.globals.getUrl('/connectivity/connectivity.ejs')) - - await browser.globals.findSpan((span) => span.name === 'documentFetch') - const connectivitySpans = browser.globals.getReceivedSpans().filter((span) => span.name === 'connectivity') - await browser.assert.strictEqual(connectivitySpans.length, 0, 'No connectivity spans cause we are offline') - - await browser.globals.emulateOffline(true) - let offlineSpan = await browser.globals.findSpan( - (span) => span.name === 'connectivity' && span.tags['online'] === 'false', - ) - let onlineSpan = await browser.globals.findSpan( - (span) => span.name === 'connectivity' && span.tags['online'] === 'true', - ) - - await browser.assert.ok(!!onlineSpan, 'Offline span exists') - await browser.assert.ok(!!offlineSpan, 'Online span exists') - await browser.globals.assertNoErrorSpans() - - browser.globals.clearReceivedSpans() - - await browser.globals.emulateOffline(false) - await browser.globals.emulateOffline(true) - - offlineSpan = await browser.globals.findSpan( - (span) => span.name === 'connectivity' && span.tags['online'] === 'false', - ) - onlineSpan = await browser.globals.findSpan( - (span) => span.name === 'connectivity' && span.tags['online'] === 'true', - ) - - await browser.assert.ok(!!onlineSpan, 'Offline span exists') - await browser.assert.ok(!!offlineSpan, 'Online span exists') - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/context/async-context.spec.js b/packages/web/integration-tests/tests/context/async-context.spec.js deleted file mode 100644 index 30048c7d..00000000 --- a/packages/web/integration-tests/tests/context/async-context.spec.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { isBrowser } = require('../../utils/helpers') - -/** - * @param {import('nightwatch').NightwatchBrowser} browser - * @param {string} filename - * @param {(clickSpan, childSpan) => void} - */ -async function runTest(browser, filename, extraChecks) { - await browser.url(browser.globals.getUrl(filename)) - - await browser.click('#btn1') - await browser.globals.waitForTestToFinish() - - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - const childSpan = await browser.globals.findSpan((span) => span.name === 'context-child') - - browser.assert.not.ok(clickSpan.parentId, 'Click span does not have a parent.') - console.log(childSpan) - browser.assert.strictEqual(childSpan.parentId, clickSpan.id, 'Child span belongs to user interaction trace.') - - await browser.globals.assertNoErrorSpans() - - if (extraChecks) { - await extraChecks(clickSpan, childSpan) - } -} - -module.exports = { - '@disabled': true, - 'afterEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'setTimeout': async function (browser) { - await runTest(browser, '/context/set-timeout.ejs') - - browser.globals.clearReceivedSpans() - - await browser.click('#btn2') - await browser.globals.waitForTestToFinish() - - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - const childSpan = await browser.globals.findSpan((span) => span.name === 'context-child') - - browser.assert.ok(!!clickSpan, 'Click has a span') - browser.assert.not.ok(childSpan.parentId, "Child span in a longer timeout doesn't have a parent") - - await browser.globals.assertNoErrorSpans() - }, - 'setImmediate': function (browser) { - if (!isBrowser(browser, 'ie')) { - // Only exists in IE - return Promise.resolve() - } - - return runTest(browser, '/context/set-immediate.ejs') - }, - 'promise constructor': async function (browser) { - if (isBrowser(browser, 'ie')) { - // Doesn't exist natively in IE - return Promise.resolve() - } - - await browser.url(browser.globals.getUrl('/context/promise.ejs')) - - for (let i = 1; i <= 5; i++) { - await browser.click('#btn' + i) - await browser.globals.waitForTestToFinish() - - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - const childSpan = await browser.globals.findSpan((span) => span.name === 'context-child') - - browser.assert.not.ok(clickSpan.parentId, `Click ${i} span does not have a parent.`) - browser.assert.strictEqual( - childSpan.parentId, - clickSpan.id, - `Child span ${i} belongs to user interaction trace.`, - ) - - await browser.globals.assertNoErrorSpans() - browser.globals.clearReceivedSpans() - } - - return true - }, - 'fetch then': function (browser) { - if (isBrowser(browser, 'ie')) { - // Doesn't exist natively in IE - return Promise.resolve() - } - - return runTest(browser, '/context/fetch-then.ejs') - }, - 'xhr event callback': function (browser) { - return runTest(browser, '/context/xhr-events.ejs') - }, - 'xhr event with removal': function (browser) { - return runTest(browser, '/context/xhr-events-removed.ejs') - }, - 'xhr onload property': function (browser) { - return runTest(browser, '/context/xhr-onload.ejs') - }, - 'mutation observer on textNode': function (browser) { - return runTest(browser, '/context/mutation-observer-textnode.ejs') - }, - 'MessagePort - addEventListener': function (browser) { - return runTest(browser, '/context/messageport-addeventlistener.ejs') - }, - 'MessagePort - onmessage': function (browser) { - return runTest(browser, '/context/messageport-onmessage.ejs') - }, - 'MessagePort: Works with cors': async function (browser) { - await browser.url(browser.globals.getUrl('/context/messageport-cors.ejs')) - await browser.globals.waitForTestToFinish() - - await browser.globals.findSpan((span) => span.name === 'message-event') - - await browser.globals.assertNoErrorSpans() - }, - 'location - hashchange': async function (browser) { - await runTest(browser, '/context/location-hash.ejs') - - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - const routeChangeSpan = await browser.globals.findSpan((span) => span.name === 'routeChange') - - browser.assert.strictEqual( - routeChangeSpan.parentId, - clickSpan.id, - 'Route change span belongs to user interaction trace.', - ) - }, -} diff --git a/packages/web/integration-tests/tests/context/framework/framework.spec.js b/packages/web/integration-tests/tests/context/framework/framework.spec.js deleted file mode 100644 index 1481093e..00000000 --- a/packages/web/integration-tests/tests/context/framework/framework.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -async function runTest(browser, filename) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl(filename)) - await browser.click('#btn1') - //TODO this causes issues after lastId guard span change - await browser.globals.waitForTestToFinish() - - const customSpan = await browser.globals.findSpan((span) => span.name === 'custom-span') - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - - browser.assert.not.ok(clickSpan.parentId, 'Click span does not have a parent.') - browser.assert.strictEqual(customSpan.parentId, clickSpan.id, 'Child span belongs to user interaction trace.') - browser.assert.strictEqual(customSpan.traceId, clickSpan.traceId, 'Spans have same traceId') - - await browser.globals.assertNoErrorSpans() -} - -module.exports = { - '@disabled': true, - 'Vue 2 with async': async function (browser) { - runTest(browser, '/context/framework/vue2.ejs') - }, - 'Vue 3 with async': async function (browser) { - runTest(browser, '/context/framework/vue3.ejs') - }, - 'React 16 with async': async function (browser) { - runTest(browser, '/context/framework/react-16.ejs') - }, - 'React 17 with async': async function (browser) { - browser.globals.clearReceivedSpans() - - await browser.url(browser.globals.getUrl('/context/framework/react-latest.ejs')) - await browser.click('#btn1') - await browser.globals.waitForTestToFinish() - - //Investigate: for some reason for react 17 there are several nested clicks - const customSpan = await browser.globals.findSpan((span) => span.name === 'custom-span') - const clickSpans = browser.globals.getReceivedSpans().filter((span) => span.name === 'click') - - clickSpans.forEach((span) => { - browser.assert.strictEqual(customSpan.traceId, span.traceId, 'Spans have same traceId') - }) - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/context/set-immediate.ejs b/packages/web/integration-tests/tests/context/set-immediate.ejs deleted file mode 100644 index 17c24f88..00000000 --- a/packages/web/integration-tests/tests/context/set-immediate.ejs +++ /dev/null @@ -1,28 +0,0 @@ - - - - - setImmediate - - <%- renderAgent({ - context: { - async: true - } - }) %> - - -

setImmediate

- - - - - - diff --git a/packages/web/integration-tests/tests/cookies/cookies.spec.js b/packages/web/integration-tests/tests/cookies/cookies.spec.js deleted file mode 100644 index 2d6111eb..00000000 --- a/packages/web/integration-tests/tests/cookies/cookies.spec.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { isBrowser } = require('../../utils/helpers') - -module.exports = { - 'afterEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'setting session cookie should work': async function (browser) { - await browser.url(browser.globals.getUrl('/cookies/cookies.ejs')) - - // This should create two streams of documentLoad sequences, all with the same sessionId but having - // two scriptInstances (one from parent, one from iframe) - const parent = await browser.globals.findSpan( - (span) => span.name === 'documentFetch' && span.tags['location.href'].includes('cookies.ejs'), - ) - await browser.assert.ok(parent.tags['browser.instance.id']) - - await browser.assert.notEqual(parent.tags['splunk.scriptInstance'], parent.tags['splunk.rumSessionId']) - - const iframe = await browser.globals.findSpan( - (span) => span.name === 'documentFetch' && span.tags['location.href'].includes('iframe.ejs'), - ) - await browser.assert.ok(iframe.tags['splunk.rumSessionId']) - await browser.assert.notEqual(iframe.tags['splunk.scriptInstance'], iframe.tags['splunk.rumSessionId']) - - // same session id & instanceId - await browser.assert.equal(parent.tags['splunk.rumSessionId'], iframe.tags['splunk.rumSessionId']) - await browser.assert.equal(parent.tags['browser.instance.id'], iframe.tags['browser.instance.id']) - - // but different scriptInstance - await browser.assert.notEqual(parent.tags['splunk.scriptInstance'], iframe.tags['splunk.scriptInstance']) - - const cookie = await browser.getCookie('_splunk_rum_sid') - await browser.assert.ok(cookie) - // FIXME we previously tested that the cookie was marked SameSite=Strict but new session implementation - // has a race between iframes and parents from the same domain. - - await browser.globals.assertNoErrorSpans() - }, - 'setting session cookie in iframe should work': async function (browser) { - await browser.url(browser.globals.getUrl('/cookies/cookies.iframe.ejs')) - - const fetchSpan = await browser.globals.findSpan( - (span) => span.name === 'documentFetch' && span.tags.app === 'iframe', - ) - await browser.assert.ok(fetchSpan.tags['splunk.rumSessionId']) - const cookie = await browser.getCookie('_splunk_rum_sid') - await browser.assert.ok(cookie) - await browser.assert.ok(fetchSpan) - if (!isBrowser(browser, 'internet explorer') && browser.globals.enableHttps) { - await browser.assert.ok(cookie.secure) - } - - if (!isBrowser(browser, { 'internet explorer': true, 'safari': true })) { - await browser.assert.equal(cookie.sameSite, browser.globals.enableHttps ? 'None' : 'Strict') - } - }, - 'setting cookieDomain via config sets it on subdomains also': async function (browser) { - // chrome: too old to remember - // edge: doesnt' work in saucelab right now for some reason? - if (isBrowser(browser, { chrome: true, microsoftedge: true })) { - return - } - - /* - We are using nip.io to let us test subdomains not sure how reliable it is, so if - you are debugging flaky test then this should be your first guess. - cookies-domain.ejs has cookieDomain set to 127.0.0.1.nip.io, cookie set via cookieDomain - should be accessible for subdomains also so when we go to test. subdomain we should find the same - cookie. - */ - const protocol = browser.globals.enableHttps ? 'https' : 'http' - await browser.url(`${protocol}://127.0.0.1.nip.io:${browser.globals.httpPort}/cookies/cookies-domain.ejs`) - const cookie = await browser.getCookie('_splunk_rum_sid') - await browser.assert.ok(cookie) - - await browser.url(`${protocol}://test.127.0.0.1.nip.io:${browser.globals.httpPort}/cookies/cookies-domain.ejs`) - - const cookie2 = await browser.getCookie('_splunk_rum_sid') - await browser.assert.strictEqual(cookie.domain, cookie2.domain) - await browser.assert.strictEqual(cookie.value, cookie2.value) - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/docload/dockload-all.spec.js b/packages/web/integration-tests/tests/docload/dockload-all.spec.js deleted file mode 100644 index 583b1cea..00000000 --- a/packages/web/integration-tests/tests/docload/dockload-all.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'resources before load event are correctly captured': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/docload/docload-all.ejs')) - - const docLoad = await browser.globals.findSpan((span) => span.name === 'documentLoad') - await browser.assert.ok(!!docLoad, 'documentLoad present') - - //t=300 to defer load event until xhr/fetch/beacon can be potentially sent so we can test that they are not sent - const resources = ['css-font-img.css', 'splunk-black.png?t=300', 'iframe.ejs', 'splunk.woff'] - const receivedSpans = await browser.globals.getReceivedSpans() - for (const urlEnd of resources) { - const resourceSpan = receivedSpans.filter( - (span) => span.tags['http.url'] && span.tags['http.url'].endsWith(urlEnd), - )[0] - await browser.assert.ok(!!resourceSpan, urlEnd) - await browser.assert.strictEqual(docLoad.traceId, resourceSpan.traceId, `${urlEnd} has correct traceId`) - } - - // no xhr, fetch, beacon span - // t=1 because otherwise xhr and fetch identical - const ignoredResources = ['/some-data', '/some-data?t=1', '/api/v2/spans'] - for (const urlEnd of ignoredResources) { - const resourceSpan = receivedSpans.filter( - (span) => - span.tags['component'] === 'document-load' && - span.tags['http.url'] && - span.tags['http.url'].endsWith(urlEnd), - )[0] - await browser.assert.not.ok(!!resourceSpan, `${urlEnd} is ignored`) - } - }, -} diff --git a/packages/web/integration-tests/tests/docload/docload.spec.js b/packages/web/integration-tests/tests/docload/docload.spec.js deleted file mode 100644 index 7d8d28d4..00000000 --- a/packages/web/integration-tests/tests/docload/docload.spec.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - '@tags': ['safari-10.1'], - 'beforeEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'documentFetch, resourceFetch, and documentLoad spans': async function (browser) { - const url = browser.globals.getUrl('/docload/docload.ejs') - await browser.url(url) - - const docFetch = await browser.globals.findSpan((span) => span.name === 'documentFetch') - const docLoad = await browser.globals.findSpan((span) => span.name === 'documentLoad') - const scriptFetch = await browser.globals.findSpan( - (span) => span.name === 'resourceFetch' && span.tags['http.url'].includes('splunk-otel-web.js'), - ) - const brokenImgFetch = await browser.globals.findSpan( - (span) => span.name === 'resourceFetch' && span.tags['http.url'].includes('/nosuchimage.jpg'), - ) - - await browser.assert.ok(docFetch, 'Checking docFetch span') - await browser.assert.ok(docLoad, 'Checking docLoad span') - await browser.assert.ok(docLoad.traceId.match(/[a-f0-9]+/), 'Checking sanity of traceId') - await browser.assert.ok(docLoad.id.match(/[a-f0-9]+/), 'Checking sanity of id') - await browser.assert.strictEqual(docFetch.traceId, docLoad.traceId) - await browser.assert.strictEqual(docFetch.parentId, docLoad.id) - - if (!browser.globals.isBrowser({ safari: { max: 10 }, ie: true })) { - await browser.assert.ok(scriptFetch, 'Checking scriptFetch span') - await browser.assert.strictEqual(scriptFetch.traceId, docLoad.traceId) - await browser.assert.strictEqual(scriptFetch.parentId, docLoad.id) - await browser.assert.strictEqual(scriptFetch.tags['component'], 'document-load') - await browser.assert.strictEqual(scriptFetch.tags['location.href'], url) - - await browser.assert.ok(brokenImgFetch, 'Checking brokenImgFetch span') - await browser.assert.strictEqual(brokenImgFetch.traceId, docLoad.traceId) - await browser.assert.strictEqual(brokenImgFetch.parentId, docLoad.id) - } - - // docFetch - await browser.assert.strictEqual(docFetch.tags['component'], 'document-load') - await browser.assert.strictEqual(docFetch.tags['location.href'], url) - await browser.timesMakeSense(docFetch.annotations, 'domainLookupStart', 'domainLookupEnd') - await browser.timesMakeSense(docFetch.annotations, 'connectStart', 'connectEnd') - await browser.timesMakeSense(docFetch.annotations, 'requestStart', 'responseStart') - await browser.timesMakeSense(docFetch.annotations, 'responseStart', 'responseEnd') - await browser.timesMakeSense(docFetch.annotations, 'fetchStart', 'responseEnd') - if (!browser.globals.isBrowser({ safari: true, ie: true })) { - await browser.assert.ok( - docFetch.tags['http.response_content_length'] >= 0, - 'Checking response_content_length', - ) - await browser.assert.ok(docFetch.tags['link.traceId'], 'Checking presence of link.traceId') - await browser.assert.ok(docFetch.tags['link.spanId'], 'Checking presence of link.spanId') - } - - // scriptFetch - if (!browser.globals.isBrowser({ safari: true, ie: true })) { - await browser.assert.ok( - scriptFetch.tags['http.response_content_length'] >= 0, - 'Checking response_content_length', - ) - await browser.assert.ok(docFetch.tags['link.traceId'], 'Checking presence of link.traceId') - await browser.assert.ok(docFetch.tags['link.spanId'], 'Checking presence of link.spanId') - } - // http.url has already been checked by the findSpan - - // documentLoad - await browser.assert.strictEqual(docLoad.tags['component'], 'document-load') - await browser.assert.strictEqual(docLoad.tags['location.href'], url) - await browser.assert.ok(docLoad.tags['screen.xy'].match(/[0-9]+x[0-9]+/), 'Checking sanity of screen.xy') - await browser.timesMakeSense(docLoad.annotations, 'domContentLoadedEventStart', 'domContentLoadedEventEnd') - await browser.timesMakeSense(docLoad.annotations, 'loadEventStart', 'loadEventEnd') - await browser.timesMakeSense(docLoad.annotations, 'fetchStart', 'domInteractive') - await browser.timesMakeSense(docLoad.annotations, 'fetchStart', 'domComplete') - - await browser.globals.assertNoErrorSpans() - }, - 'ignoring resource URLs': async function (browser) { - await browser.url(browser.globals.getUrl('/docload/docload-ignored.ejs')) - - await browser.globals.findSpan((span) => span.name === 'documentFetch') - const url = browser.globals.getUrl('/', []) - await browser.assert.not.ok( - browser.globals - .getReceivedSpans() - .find((span) => span.tags['http.url'] === url + 'non-impactful-resource.jpg'), - ) - }, - 'module can be disabled': async function (browser) { - await browser.url(browser.globals.getUrl('/docload/docload.ejs?disableInstrumentation=document')) - await browser.globals.waitForTestToFinish() - - await browser.assert.ok(await browser.globals.findSpan((span) => span.name === 'eventListener.error')) - - const SPAN_TYPES = ['documentFetch', 'documentLoad', 'resourceFetch'] - await browser.assert.not.ok( - browser.globals.getReceivedSpans().find((span) => SPAN_TYPES.includes(span.name)), - 'No spans with docload module type', - ) - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/errors/index.spec.js b/packages/web/integration-tests/tests/errors/index.spec.js deleted file mode 100644 index 5182d039..00000000 --- a/packages/web/integration-tests/tests/errors/index.spec.js +++ /dev/null @@ -1,232 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { isBrowser } = require('../../utils/helpers') - -module.exports = { - 'beforeEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'DOM resource 4xx': async function (browser) { - await browser.url(browser.globals.getUrl('/errors/views/resource-4xx.ejs')) - - const errorSpan = await browser.globals.findSpan((s) => s.name === 'eventListener.error') - await browser.assert.ok(!!errorSpan, 'Checking if error span was found.') - - const tags = errorSpan.tags - await browser.assert.strictEqual(tags['component'], 'error') - await browser.assert.strictEqual(tags['error.type'], 'error') - await browser.assert.strictEqual(tags['target_element'], 'IMG') - await browser.assert.strictEqual(tags['target_xpath'], '//html/body/img') - await browser.assert.ok( - tags['target_src'].endsWith('/nonexistent.png'), - `Checking target_src: ${tags['target_src']}`, - ) - }, - 'JS syntax error': async function (browser) { - await browser.url(browser.globals.getUrl('/errors/views/js-syntax-error.ejs')) - - const errorSpan = await browser.globals.findSpan((s) => s.name === 'onerror') - await browser.assert.ok(!!errorSpan, 'Checking presence of error span.') - - const tags = errorSpan.tags - await browser.assert.strictEqual(tags['component'], 'error') - await browser.assert.strictEqual(tags['error'], 'true') - await browser.assert.strictEqual( - tags['error.object'], - isBrowser(browser, 'internet explorer') ? 'String' : 'SyntaxError', - ) - - switch (browser.options.desiredCapabilities.browserName.toLowerCase()) { - case 'chrome': - await browser.assert.strictEqual(tags['error.message'], "Unexpected token ';'") - break - case 'firefox': - await browser.assert.strictEqual(tags['error.message'], "expected expression, got ';'") - break - case 'safari': - await browser.assert.strictEqual(tags['error.message'], "Unexpected token ';'") - break - case 'internet explorer': - await browser.assert.strictEqual(tags['error.message'], 'Syntax error') - break - } - }, - 'JS unhandled error': async function (browser) { - await browser.url(browser.globals.getUrl('/errors/views/unhandled-error.ejs')) - - const errorSpan = await browser.globals.findSpan((s) => s.name === 'onerror') - await browser.assert.ok(errorSpan, 'Checking presence of error span.') - - const tags = errorSpan.tags - await browser.assert.strictEqual(tags['component'], 'error') - await browser.assert.strictEqual(tags['error'], 'true') - await browser.assert.strictEqual(tags['error.object'], 'TypeError') - - switch (browser.options.desiredCapabilities.browserName.toLowerCase()) { - case 'chrome': - await browser.assert.strictEqual( - tags['error.message'], - "Cannot set properties of null (setting 'prop1')", - ) - break - case 'firefox': - await browser.assert.strictEqual(tags['error.message'], 'test is null') - break - case 'safari': - await browser.assert.strictEqual( - tags['error.message'], - "null is not an object (evaluating 'test.prop1 = true')", - ) - break - case 'internet explorer': - await browser.assert.strictEqual( - tags['error.message'], - "Unable to set property 'prop1' of undefined or null reference", - ) - break - } - }, - 'unhandled promise rejection': async function (browser) { - if (isBrowser(browser, 'internet explorer')) { - return // No native promise - } - - await browser.url(browser.globals.getUrl('/errors/views/unhandled-rejection.ejs')) - - const errorSpan = await browser.globals.findSpan((s) => s.name === 'unhandledrejection') - await browser.assert.ok(!!errorSpan, 'Checking presence of error span.') - - const tags = errorSpan.tags - await browser.assert.strictEqual(tags['component'], 'error') - await browser.assert.strictEqual(tags['error'], 'true') - await browser.assert.strictEqual(tags['error.object'], 'String') - await browser.assert.strictEqual(tags['error.message'], 'rejection-value') - }, - 'manual console.error': async function (browser) { - const browserName = browser.options.desiredCapabilities.browserName.toLowerCase() - - const url = browser.globals.getUrl('/errors/views/console-error.ejs') - await browser.url(url) - - const errorSpan = await browser.globals.findSpan((s) => s.name === 'console.error') - await browser.assert.ok(!!errorSpan, 'Checking presence of error span.') - - const tags = errorSpan.tags - await browser.assert.strictEqual(tags['component'], 'error') - await browser.assert.strictEqual(tags['error'], 'true') - await browser.assert.strictEqual(tags['error.object'], 'TypeError') - - const ERROR_MESSAGE_MAP = { - 'safari': "null is not an object (evaluating 'someNull.anyField = 'value'')", - 'chrome': "Cannot set properties of null (setting 'anyField')", - 'microsoftedge': "Cannot set properties of null (setting 'anyField')", - 'firefox': 'someNull is null', - 'internet explorer': "Unable to set property 'anyField' of undefined or null reference", - } - await browser.assert.strictEqual(tags['error.message'], ERROR_MESSAGE_MAP[browserName]) - - const ERROR_STACK_MAP = { - 'safari': `global code@${url}:62:15`, - 'chrome': `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:62:25`, - 'microsoftedge': `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:62:25`, - 'firefox': `@${url}:62:7\n`, - 'internet explorer': `TypeError: Unable to set property 'anyField' of undefined or null reference\n at Global code (${url}:62:7)`, - } - await browser.assert.strictEqual(tags['error.stack'], ERROR_STACK_MAP[browserName]) - }, - 'SplunkRum.error': async function (browser) { - const browserName = browser.options.desiredCapabilities.browserName.toLowerCase() - const url = browser.globals.getUrl('/errors/views/splunkrum-error.ejs') - await browser.url(url) - - const errorSpan = await browser.globals.findSpan((s) => s.name === 'SplunkRum.error') - await browser.assert.ok(!!errorSpan, 'Checking presence of error span.') - - const tags = errorSpan.tags - await browser.assert.strictEqual(tags['component'], 'error') - await browser.assert.strictEqual(tags['error'], 'true') - await browser.assert.strictEqual(tags['error.object'], 'TypeError') - - const ERROR_MESSAGE_MAP = { - 'safari': "null is not an object (evaluating 'someNull.anyField = 'value'')", - 'chrome': "Cannot set properties of null (setting 'anyField')", - 'microsoftedge': "Cannot set properties of null (setting 'anyField')", - 'firefox': 'someNull is null', - 'internet explorer': "Unable to set property 'anyField' of undefined or null reference", - } - - await browser.assert.strictEqual(tags['error.message'], ERROR_MESSAGE_MAP[browserName]) - - const ERROR_STACK_MAP = { - 'safari': `global code@${url}:62:15`, - 'microsoftedge': `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:62:25`, - 'chrome': `TypeError: Cannot set properties of null (setting 'anyField')\n at ${url}:62:25`, - 'firefox': `@${url}:62:7\n`, - 'internet explorer': `TypeError: Unable to set property 'anyField' of undefined or null reference\n at Global code (${url}:62:7)`, - } - await browser.assert.strictEqual(tags['error.stack'], ERROR_STACK_MAP[browserName]) - }, - 'module can be disabled': async function (browser) { - await browser.url(browser.globals.getUrl('/errors/views/unhandled-error.ejs')) - await browser.globals.waitForTestToFinish() - - browser.assert.ok( - !!browser.globals.getReceivedSpans().find(({ name }) => name === 'onerror'), - 'Checking presence of error span.', - ) - - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/errors/views/unhandled-error.ejs?disableInstrumentation=errors')) - await browser.globals.waitForTestToFinish() - - browser.assert.not.ok( - browser.globals.getReceivedSpans().find(({ name }) => name === 'onerror'), - 'Checking lack of error span.', - ) - }, - 'minified script with source map id': async function (browser) { - await browser.url(browser.globals.getUrl('/errors/views/minified_file_errors.ejs')) - // click on button1 to trigger error - await browser.click('#button1') - await browser.pause(1000) - const errorSpan1 = await browser.globals.findSpan((s) => s.name === 'onerror') - await browser.assert.ok(!!errorSpan1, 'Checking presence of error span.') - await browser.assert.strictEqual(errorSpan1.tags['error.message'], 'Error from script1.js') - // check errorSpan2.tags['error.source_map_ids'] contains the strings script1.min.js and the hash text - await browser.assert.ok(errorSpan1.tags['error.source_map_ids'].includes('script1.min.js')) - await browser.assert.ok( - errorSpan1.tags['error.source_map_ids'].includes( - '9663c60664c425cef3b141c167e9b324240ce10ae488726293892b7130266a6b', - ), - ) - // clear spans and do same for button2 - browser.globals.clearReceivedSpans() - await browser.click('#button2') - await browser.pause(1000) - const errorSpan2 = await browser.globals.findSpan((s) => s.name === 'onerror') - await browser.assert.ok(!!errorSpan2, 'Checking presence of error span.') - await browser.assert.strictEqual(errorSpan2.tags['error.message'], 'Error from script2.js') - await browser.assert.ok(errorSpan2.tags['error.source_map_ids'].includes('script2.min.js')) - await browser.assert.ok( - errorSpan2.tags['error.source_map_ids'].includes( - '9793573cdc2ab308a0b1996bea14253ec8832bc036210475ded0813cafa27066', - ), - ) - }, -} diff --git a/packages/web/integration-tests/tests/fetch/fetch.spec.js b/packages/web/integration-tests/tests/fetch/fetch.spec.js deleted file mode 100644 index 38abb2b6..00000000 --- a/packages/web/integration-tests/tests/fetch/fetch.spec.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - '@tags': ['safari-10.1', 'skip-ie11'], - 'beforeEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'span created for fetch includes all properties': async function (browser) { - await browser.url(browser.globals.getUrl('/fetch/fetch.ejs')) - - const fetchSpan = await browser.globals.findSpan( - (span) => span.tags['http.url'] && span.tags['http.url'].endsWith('/some-data'), - ) - await browser.assert.ok(!!fetchSpan, 'Fetch span found.') - await browser.assert.strictEqual(fetchSpan.tags['component'], 'fetch') - await browser.assert.strictEqual(fetchSpan.tags['http.status_code'], '200') - await browser.assert.strictEqual(fetchSpan.tags['http.status_text'], 'OK') - await browser.assert.strictEqual(fetchSpan.tags['http.method'], 'GET') - - if (browser.options.desiredCapabilities.browserName !== 'Safari') { - await browser.assert.strictEqual(fetchSpan.tags['http.response_content_length'], '49') - } - - await browser.assert.ok(fetchSpan.tags['link.traceId'], 'got link.traceId') - await browser.assert.ok(fetchSpan.tags['link.spanId'], 'got link.spanId') - if ( - browser.options.desiredCapabilities.browserName !== 'Safari' && - browser.options.desiredCapabilities.browser_version !== '10.1' - ) { - await browser.timesMakeSense(fetchSpan.annotations, 'domainLookupStart', 'domainLookupEnd') - await browser.timesMakeSense(fetchSpan.annotations, 'connectStart', 'connectEnd') - if (browser.globals.enableHttps) { - await browser.timesMakeSense(fetchSpan.annotations, 'secureConnectionStart', 'connectEnd') - } - - await browser.timesMakeSense(fetchSpan.annotations, 'requestStart', 'responseStart') - await browser.timesMakeSense(fetchSpan.annotations, 'responseStart', 'responseEnd') - await browser.timesMakeSense(fetchSpan.annotations, 'fetchStart', 'responseEnd') - } - - await browser.globals.assertNoErrorSpans() - }, - 'fetch request can be ignored': async function (browser) { - await browser.url(browser.globals.getUrl('/fetch/fetch-ignored.ejs')) - - await browser.globals.findSpan((span) => span.name === 'guard-span') - await browser.globals.assertNoErrorSpans() - - await browser.assert.not.ok( - browser.globals - .getReceivedSpans() - .find((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('/some-data')), - ) - await browser.assert.not.ok( - browser.globals - .getReceivedSpans() - .find((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('/no-server-timings')), - ) - }, - 'fetch reported over CORS': async function (browser) { - const backend2 = await browser.globals.buildInstrumentationBackend() - await browser.url( - backend2.getUrl('/fetch/fetch.ejs', undefined, { beaconPort: browser.globals.httpPort }).toString(), - ) - - const fetchSpan = await browser.globals.findSpan( - (span) => span.tags['http.url'] && span.tags['http.url'].endsWith('/some-data'), - ) - await browser.assert.ok(!!fetchSpan, 'Fetch span found.') - await browser.assert.strictEqual(fetchSpan.tags['component'], 'fetch') - await browser.assert.strictEqual(fetchSpan.tags['http.status_code'], '200') - await browser.assert.strictEqual(fetchSpan.tags['http.status_text'], 'OK') - await browser.assert.strictEqual(fetchSpan.tags['http.method'], 'GET') - await browser.assert.strictEqual(fetchSpan.tags['http.host'], `${backend2.httpHostname}:${backend2.httpPort}`) - - await browser.globals.assertNoErrorSpans() - await backend2.close() - }, - 'request body exists in request object (open-telemetry/opentelemetry-js#2411)': async function (browser) { - await browser.url(browser.globals.getUrl('/fetch/fetch-post.ejs')) - await browser.globals.findSpan((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('/echo')) - - browser.expect.element('#result').text.to.equal('{"test":true}') - }, - 'can be disabled (with xhr switch)': async function (browser) { - await browser.url(browser.globals.getUrl('/fetch/fetch.ejs')) - await browser.globals.waitForTestToFinish() - - await browser.assert.ok( - !!browser.globals - .getReceivedSpans() - .find((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('/some-data')), - ) - - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/fetch/fetch.ejs?disableInstrumentation=xhr,fetch')) - await browser.globals.waitForTestToFinish() - - await browser.assert.not.ok( - browser.globals - .getReceivedSpans() - .find((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('/some-data')), - ) - }, -} diff --git a/packages/web/integration-tests/tests/init/init.spec.js b/packages/web/integration-tests/tests/init/init.spec.js deleted file mode 100644 index 63afe313..00000000 --- a/packages/web/integration-tests/tests/init/init.spec.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - '@tags': ['safari-10.1'], - 'No spans sent when no beacon url set': async function (browser) { - await browser.url(browser.globals.getUrl('/init/nobeacon.ejs')) - const span = await browser.globals.findSpan(() => true) - await browser.assert.not.ok(span) - }, - 'attribute-related config should work': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/init/attributes.ejs')) - const atts = await browser.globals.findSpan((span) => span.name === 'documentLoad') - await browser.assert.ok(atts) - await browser.assert.strictEqual(atts.tags['app'], 'custom-app') - await browser.assert.strictEqual(atts.tags['environment'], 'custom-environment') - await browser.assert.strictEqual(atts.tags['key1'], 'value1') - await browser.assert.strictEqual(atts.tags['key2'], 'value2') - - await browser.click('#clickToChangeAttributes') - - const atts2 = await browser.globals.findSpan((span) => span.name === 'attributes-set') - await browser.assert.ok(atts2) - // environment still set (not cleared by setGlobalAttributes call) - await browser.assert.strictEqual(atts2.tags['environment'], 'custom-environment') - await browser.assert.strictEqual(atts2.tags['key1'], 'newvalue1') - await browser.assert.strictEqual(atts2.tags['key2'], 'value2') - - const attrsNotificationSpan = await browser.globals.findSpan((span) => span.name === 'attributes-changed') - await browser.assert.ok(attrsNotificationSpan) - - const notifiedAttrs = JSON.parse(attrsNotificationSpan.tags['payload']).attributes - await browser.assert.strictEqual(notifiedAttrs.environment, 'custom-environment') - await browser.assert.strictEqual(notifiedAttrs.key1, 'newvalue1') - await browser.assert.strictEqual(notifiedAttrs.key2, 'value2') - - await browser.click('#clickToResetAttributes') - // no argument setGlobalAttributes() call will set empty attributes - const atts3 = await browser.globals.findSpan((span) => span.name === 'attributes-reset') - await browser.assert.ok(atts3) - await browser.assert.strictEqual(atts3.tags['environment'], undefined) - await browser.assert.strictEqual(atts3.tags['key1'], undefined) - await browser.assert.strictEqual(atts3.tags['key1'], undefined) - - await browser.globals.assertNoErrorSpans() - }, - 'instrumentations.errors controls error capture': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/init/captureErrors.ejs')) - const errorGuard = await browser.globals.findSpan((span) => span.name === 'error-guard-span') - await browser.assert.ok(errorGuard) - await browser.globals.assertNoErrorSpans() - }, - 'environment % resource attrs still get set if no global attributes': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/init/attributes-no-globals.ejs')) - - const atts = await browser.globals.findSpan((span) => span.name === 'documentLoad') - await browser.assert.ok(atts) - await browser.assert.strictEqual(atts.tags['app'], 'custom-app') - await browser.assert.strictEqual(atts.tags['environment'], 'custom-environment') - - // Set as a resource, zipkin exporter should merge into tags - await browser.assert.strictEqual(atts.tags['telemetry.sdk.name'], '@splunk/otel-web') - await browser.assert.strictEqual(atts.tags['telemetry.sdk.language'], 'webjs') - await browser.assert.strictEqual(atts.tags['telemetry.sdk.version'], browser.globals.rumVersion) - await browser.assert.strictEqual(atts.tags['splunk.rumVersion'], browser.globals.rumVersion) - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/longtask/index.spec.js b/packages/web/integration-tests/tests/longtask/index.spec.js deleted file mode 100644 index c775b115..00000000 --- a/packages/web/integration-tests/tests/longtask/index.spec.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -function browserIsCompatible(browser) { - return browser.globals.isBrowser({ chrome: true, microsoftedge: true }) -} - -module.exports = { - '@tags': ['skip-ie11'], - 'beforeEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'reports a longtask': async function (browser) { - await browser.url(browser.globals.getUrl('/longtask/index.ejs')) - await browser.click('#btnLongtask') - - if (browserIsCompatible(browser)) { - const longtaskSpan = await browser.globals.findSpan((span) => span.name === 'longtask') - await browser.assert.ok(!!longtaskSpan, 'Checking longtask span presence.') - await browser.assert.strictEqual(longtaskSpan.tags['component'], 'splunk-longtask', 'component') - // Edge can also report this with unknown value - await browser.assert.strictEqual( - ['self', 'unknown'].includes(longtaskSpan.tags['longtask.name']), - true, - 'longtask.name', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.entry_type'], - 'longtask', - 'longtask.entry_type', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.attribution.name'], - 'unknown', - 'longtask.attribution.name', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.attribution.entry_type'], - 'taskattribution', - 'longtask.attribution.entry_type', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.attribution.start_time'], - '0', - 'longtask.attribution.start_time', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.attribution.duration'], - '0', - 'longtask.attribution.duration', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.attribution.container_type'], - 'window', - 'longtask.attribution.container_type', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.attribution.container_src'], - '', - 'longtask.attribution.container_src', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.attribution.container_id'], - '', - 'longtask.attribution.container_id', - ) - await browser.assert.strictEqual( - longtaskSpan.tags['longtask.attribution.container_name'], - '', - 'longtask.attribution.container_name', - ) - - const duration = parseFloat(longtaskSpan.tags['longtask.duration']) - await browser.assert.ok(duration > 50, `Duration (${duration}) must be over 50ms by definition.`) - // note: our longtask simulator targets 55ms, but headless chrome doesn't understand throttling - await browser.assert.ok(duration < 1000, `Duration (${duration}) must be less than 1s by definition.`) - await browser.assert.strictEqual( - longtaskSpan.duration, - duration * 1000, - 'Span duration matches longtask duration', - ) - } - - await browser.globals.assertNoErrorSpans() - }, - 'reports buffered longtask': async function (browser) { - if (!browserIsCompatible(browser)) { - return // Skip this test - } - - await browser.url(browser.globals.getUrl('/longtask/buffered.ejs')) - const longtaskSpan = await browser.globals.findSpan((span) => span.name === 'longtask') - const duration = parseFloat(longtaskSpan.tags['longtask.duration']) - await browser.assert.strictEqual( - longtaskSpan.duration, - duration * 1000, - 'Span duration matches longtask duration', - ) - }, - 'can be disabled': async function (browser) { - if (!browserIsCompatible(browser)) { - return // Skip this test - } - - await browser.url(browser.globals.getUrl('/longtask/index.ejs')) - await browser.click('#btnLongtask') - await browser.globals.waitForTestToFinish() - - browser.assert.ok( - !!browser.globals.getReceivedSpans().find(({ name }) => name === 'longtask'), - 'Checking presence of longtask span.', - ) - await browser.globals.assertNoErrorSpans() - - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/longtask/index.ejs?disableInstrumentation=longtask')) - await browser.click('#btnLongtask') - await browser.globals.waitForTestToFinish() - - browser.assert.not.ok( - browser.globals.getReceivedSpans().find(({ name }) => name === 'longtask'), - 'Checking presence of longtask span.', - ) - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/resource-observer/resource-obs.spec.js b/packages/web/integration-tests/tests/resource-observer/resource-obs.spec.js deleted file mode 100644 index 09edcc41..00000000 --- a/packages/web/integration-tests/tests/resource-observer/resource-obs.spec.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { isBrowser } = require('../../utils/helpers') -const { inspect } = require('util') - -module.exports = { - '@disabled': true, // too flaky, will fix when migrating from nightwatch - 'beforeEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'should report resource loads happening after page load': async function (browser) { - if ( - isBrowser(browser, { - safari: true, - edge: true, - ie: true, - }) - ) { - return - } - - await browser.url(browser.globals.getUrl('/resource-observer/resources.ejs')) - - await browser.globals.findSpan((span) => span.name === 'guard-span') - - const plAgentSpans = browser.globals - .getReceivedSpans() - .filter((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('splunk-otel-web.js')) - const plImageSpans = browser.globals - .getReceivedSpans() - .filter((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('no-cache.png')) - - await browser.assert.strictEqual(plAgentSpans.length, 1) - await browser.assert.strictEqual(plImageSpans.length, 1) - - const imageSpan = await browser.globals.findSpan( - (span) => span.tags['http.url'] && span.tags['http.url'].endsWith('splunk-black.png?t=100'), - ) - - await browser.assert.ok(!!imageSpan, 'Image span found.') - await browser.assert.strictEqual(imageSpan.tags['component'], 'splunk-post-doc-load-resource') - const imgUrl = browser.globals.getUrl('/utils/devServer/assets/', []) + 'splunk-black.png?t=100' - await browser.assert.strictEqual(imageSpan.tags['http.url'], imgUrl) - await browser.assert.strictEqual(imageSpan.annotations.length, 9, 'Missing network events') - - const scriptSpan = await browser.globals.findSpan( - (span) => span.tags['http.url'] && span.tags['http.url'].endsWith('test.js'), - ) - - await browser.assert.strictEqual(scriptSpan.tags['component'], 'splunk-post-doc-load-resource') - const scriptUrl = browser.globals.getUrl('/utils/devServer/assets/', []) + 'test.js' - await browser.assert.strictEqual(scriptSpan.tags['http.url'], scriptUrl) - await browser.assert.strictEqual(scriptSpan.annotations.length, 9, 'Missing network events') - }, - 'resources can be ignored': async function (browser) { - if ( - isBrowser(browser, { - safari: true, - edge: true, - ie: true, - }) - ) { - return - } - - await browser.url(browser.globals.getUrl('/resource-observer/resources-ignored.ejs')) - - await browser.globals.findSpan((span) => span.name === 'guard-span') - const url = browser.globals.getUrl('/utils/devServer/assets/', []) - await browser.assert.not.ok( - browser.globals - .getReceivedSpans() - .find( - (span) => - span.tags['http.url'] === url + 'test.js' && - span.tags['component'] === 'splunk-post-doc-load-resource', - ), - ) - const imgSpan = browser.globals - .getReceivedSpans() - .find( - (span) => - span.tags['http.url'] === url + 'splunk-black.png' && - span.tags['component'] === 'splunk-post-doc-load-resource', - ) - await browser.assert.not.ok(imgSpan) - }, - 'should create one span for cached resource': async function (browser) { - if ( - isBrowser(browser, { - safari: true, - edge: true, - ie: true, - }) - ) { - return - } - - await browser.url(browser.globals.getUrl('/resource-observer/resources-twice.ejs')) - await browser.globals.findSpan((span) => span.name === 'guard-span') - - const imageSpans = await browser.globals - .getReceivedSpans() - .filter((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('splunk-black.png')) - - // for debugging flaky tests - if (imageSpans.length !== 0) { - console.log('imageSpans.length') - console.log(inspect(imageSpans, { depth: 10 })) - } - - await browser.assert.strictEqual(imageSpans.length, 1) - await browser.assert.strictEqual(imageSpans[0].tags['component'], 'document-load') - }, - 'should create two spans for non cached resource': async function (browser) { - if ( - isBrowser(browser, { - safari: true, - edge: true, - ie: true, - }) - ) { - return - } - - if (browser.options.desiredCapabilities.browserName.toLowerCase() === 'firefox') { - // Can't get fx to stop caching the image. - return - } - - await browser.url(browser.globals.getUrl('/resource-observer/resources-twice-no-cache.ejs')) - await browser.globals.findSpan((span) => span.name === 'guard-span') - - const imageSpans = await browser.globals - .getReceivedSpans() - .filter((span) => span.tags['http.url'] && span.tags['http.url'].endsWith('no-cache.png')) - - // for debugging flaky tests - if (imageSpans.length !== 2) { - console.log('imageSpans.length') - console.log(inspect(imageSpans, { depth: 10 })) - } - - await browser.assert.strictEqual(imageSpans.length, 2) - const docLoadImage = imageSpans.find((span) => span.tags['component'] === 'document-load') - const afterLoadImage = imageSpans.find((span) => span.tags['component'] === 'splunk-post-doc-load-resource') - - await browser.assert.notEqual(docLoadImage.traceId, afterLoadImage.traceId) - }, - 'should propagate tracing context to created spans': async function (browser) { - if ( - isBrowser(browser, { - safari: true, - edge: true, - ie: true, - }) - ) { - return - } - - await browser.url(browser.globals.getUrl('/resource-observer/resources-custom-context.ejs')) - await browser.globals.findSpan((span) => span.name === 'guard-span') - - const plImageRootSpan = await browser.globals - .getReceivedSpans() - .find( - (span) => - span.tags['http.url'] && - span.tags['http.url'].endsWith('splunk-black.png') && - span.tags['component'] === 'splunk-post-doc-load-resource', - ) - const plImageParentSpan = await browser.globals.getReceivedSpans().find((span) => span.name === 'image-parent') - const plImageChildSpan = await browser.globals - .getReceivedSpans() - .find( - (span) => - span.tags['http.url'] && - span.tags['http.url'].endsWith('splunk-black.svg') && - span.tags['component'] === 'splunk-post-doc-load-resource', - ) - await browser.assert.ok(plImageRootSpan) - await browser.assert.ok(plImageParentSpan) - await browser.assert.ok(plImageChildSpan) - - await browser.assert.notEqual(plImageRootSpan.traceId, plImageParentSpan.traceId) - await browser.assert.not.ok(plImageRootSpan.parentId) - await browser.assert.equal(plImageParentSpan.traceId, plImageChildSpan.traceId) - await browser.assert.equal(plImageChildSpan.parentId, plImageParentSpan.id) - - const plScriptRootSpan = await browser.globals - .getReceivedSpans() - .find( - (span) => - span.tags['http.url'] && - span.tags['http.url'].endsWith('test.js') && - span.tags['component'] === 'splunk-post-doc-load-resource', - ) - const plScriptParentSpan = await browser.globals - .getReceivedSpans() - .find((span) => span.name === 'script-parent') - const plScriptChildSpan = await browser.globals - .getReceivedSpans() - .find( - (span) => - span.tags['http.url'] && - span.tags['http.url'].endsWith('test-alt.js') && - span.tags['component'] === 'splunk-post-doc-load-resource', - ) - await browser.assert.ok(plScriptRootSpan) - await browser.assert.ok(plScriptParentSpan) - await browser.assert.ok(plScriptChildSpan) - - await browser.assert.notEqual(plScriptRootSpan.traceId, plScriptParentSpan.traceId) - await browser.assert.not.ok(plScriptRootSpan.parentId) - await browser.assert.equal(plScriptParentSpan.traceId, plScriptChildSpan.traceId) - await browser.assert.equal(plScriptChildSpan.parentId, plScriptParentSpan.id) - }, - "doesn't crash when postload instrumentation is disabled": async function (browser) { - await browser.url(browser.globals.getUrl('/resource-observer/resources-custom-context.ejs')) - await browser.click('#btn1') - - const click = await browser.globals.findSpan((span) => span.name === 'click') - await browser.assert.ok(click) - await browser.global.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/server-timing/index.spec.js b/packages/web/integration-tests/tests/server-timing/index.spec.js deleted file mode 100644 index a9c1a24a..00000000 --- a/packages/web/integration-tests/tests/server-timing/index.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'traceId should be attached to documentFetch span if Server-Timing was sent': async function (browser) { - // TODO: restructure into a proper feature support matrix - const UNSUPPORTED_BROWSERS = ['Safari', 'IE', 'iPhone'] - const currentBrowser = browser.options.desiredCapabilities.browserName - if (UNSUPPORTED_BROWSERS.includes(currentBrowser)) { - return - } - - // preload the page once for firefox - // in some cases firefox will report negative fetchStart for localhost pages - // in that case opentelemetry-plugin-document-load will not report any spans - await browser.url(browser.globals.baseUrl) - - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/server-timing/index.ejs')) - const expectedTraceId = browser.globals.getLastServerTiming().traceId - const docFetchSpan = await browser.globals.findSpan( - (span) => span.name === 'documentFetch' && span.tags['link.traceId'] === expectedTraceId, - ) - - await browser.assert.strictEqual(docFetchSpan.tags['link.traceId'], expectedTraceId) - await browser.assert.strictEqual( - docFetchSpan.tags['location.href'], - browser.globals.getUrl('/server-timing/index.ejs'), - ) - await browser.assert.strictEqual(docFetchSpan.tags['app'], 'splunk-otel-js-dummy-app') - await browser.assert.strictEqual(docFetchSpan.tags['component'], 'document-load') - await browser.assert.strictEqual(docFetchSpan.tags['splunk.rumVersion'], browser.globals.rumVersion) - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/synthetics/index.spec.js b/packages/web/integration-tests/tests/synthetics/index.spec.js deleted file mode 100644 index 4c216699..00000000 --- a/packages/web/integration-tests/tests/synthetics/index.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { buildBackendContext } = require('../../utils/globals') - -module.exports = { - 'applies Synthetics Run Id attribute on all spans': async function (browser) { - const backendCtx = await buildBackendContext(browser) - - browser.globals.clearReceivedSpans() - await browser.url( - backendCtx - .getUrl('/synthetics/index.ejs', undefined, { - syntheticsRunId: 'abcd1234'.repeat(4), - }) - .toString(), - ) - - const guardSpan = await backendCtx.findSpan((s) => s.name === 'guard-span') - await browser.assert.ok(guardSpan, 'Ensuring guard-span has arrived before we start inspecting spans.') - - for (const span of backendCtx.getReceivedSpans()) { - await browser.assert.strictEqual(span.tags['Synthetics-RunId'], 'abcd1234'.repeat(4)) - } - - await backendCtx._closeBackend() - }, - - 'does not apply Synthetics Run Id attribute on all spans if not included': async function (browser) { - const backendCtx = await buildBackendContext(browser) - - backendCtx.clearReceivedSpans() - await browser.url(backendCtx.getUrl('/synthetics/index.ejs')) - - const guardSpan = await backendCtx.findSpan((s) => s.name === 'guard-span') - await browser.assert.ok(guardSpan, 'Ensuring guard-span has arrived before we start inspecting spans.') - - for (const span of backendCtx.getReceivedSpans()) { - await browser.assert.strictEqual(span.tags['Synthetics-RunId'], undefined) - } - - await backendCtx._closeBackend() - }, -} diff --git a/packages/web/integration-tests/tests/user-interaction/causality.spec.js b/packages/web/integration-tests/tests/user-interaction/causality.spec.js deleted file mode 100644 index b049da9d..00000000 --- a/packages/web/integration-tests/tests/user-interaction/causality.spec.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'beforeEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'handles causality of a mouse click': async function (browser) { - await browser.url(browser.globals.getUrl('/user-interaction/causality.ejs')) - - // Do a click so web-vitals can run and then remove it's event listeners - await browser.click('body') - - await browser.click('#btn1') - - const clickSpan = await browser.globals.findSpan( - (span) => span.name === 'click' && span.tags['target_xpath'] === '//*[@id="btn1"]', - ) - await browser.assert.ok(!!clickSpan, 'Checking click span presence.') - - const fetchSpan = await browser.globals.findSpan( - (span) => span.tags['http.url'] === browser.globals.getUrl('/some-data', []), - ) - await browser.assert.ok(!!fetchSpan, 'Checking fetch span presence.') - - await browser.assert.not.ok(clickSpan.parentId, 'Click span does not have a parent.') - await browser.assert.strictEqual( - fetchSpan.parentId, - clickSpan.id, - 'Fetch span belongs to user interaction trace.', - ) - - await browser.globals.assertNoErrorSpans() - }, - 'can be disabled': async function (browser) { - await browser.url(browser.globals.getUrl('/user-interaction/causality.ejs')) - await browser.click('#btn1') - await browser.globals.waitForTestToFinish() - - browser.assert.ok( - !!(await browser.globals.getReceivedSpans().find(({ name }) => name === 'click')), - 'click span recorded', - ) - - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/user-interaction/causality.ejs?disableInstrumentation=interactions')) - await browser.click('#btn1') - await browser.globals.waitForTestToFinish() - - browser.assert.not.ok( - await browser.globals.getReceivedSpans().find(({ name }) => name === 'click'), - 'lack of click span', - ) - }, -} diff --git a/packages/web/integration-tests/tests/user-interaction/forms.spec.js b/packages/web/integration-tests/tests/user-interaction/forms.spec.js deleted file mode 100644 index 898cbf19..00000000 --- a/packages/web/integration-tests/tests/user-interaction/forms.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'handles form submit': async function (browser) { - browser.globals.clearReceivedSpans() - - const startUrl = browser.globals.getUrl('/user-interaction/forms.ejs') - await browser.url(startUrl) - await browser.click('#btnSubmit') - - const submitSpan = await browser.globals.findSpan((span) => span.name === 'submit') - await browser.assert.ok(!!submitSpan, 'Checking form submission span presence.') - - await browser.assert.strictEqual(submitSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(submitSpan.tags['event_type'], 'submit') - await browser.assert.strictEqual(submitSpan.tags['target_element'], 'FORM') - await browser.assert.strictEqual(submitSpan.tags['target_xpath'], '//*[@id="form1"]') - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/user-interaction/history.spec.js b/packages/web/integration-tests/tests/user-interaction/history.spec.js deleted file mode 100644 index b97d3ed1..00000000 --- a/packages/web/integration-tests/tests/user-interaction/history.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { isBrowser } = require('../../utils/helpers') - -module.exports = { - 'handles hash navigation': async function (browser) { - browser.globals.clearReceivedSpans() - - const startUrl = browser.globals.getUrl('/user-interaction/history.ejs') - await browser.url(startUrl) - await browser.click('#btnGoToPage') - - const navigationSpan = await browser.globals.findSpan((span) => span.name === 'routeChange') - await browser.assert.ok(!!navigationSpan, 'Checking navigation span presence.') - - await browser.assert.strictEqual(navigationSpan.tags['component'], 'user-interaction') - if (!isBrowser(browser, 'ie')) { - await browser.assert.strictEqual(navigationSpan.tags['prev.href'], startUrl) - } - - await browser.assert.strictEqual(navigationSpan.tags['location.href'], startUrl + '#another-page') - - await browser.globals.assertNoErrorSpans() - }, - 'handles history navigation': async function (browser) { - browser.globals.clearReceivedSpans() - - const startUrl = browser.globals.getUrl('/user-interaction/history.ejs') - await browser.url(startUrl) - await browser.click('#btnGoToPage') - await browser.globals.findSpan((span) => span.name === 'routeChange') - - browser.globals.clearReceivedSpans() - await browser.click('#btnGoBack') - - const navigationBackSpan = await browser.globals.findSpan((span) => span.name === 'routeChange') - await browser.assert.ok(!!navigationBackSpan, 'Checking backwards navigation span presence.') - - await browser.assert.strictEqual(navigationBackSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(navigationBackSpan.tags['prev.href'], startUrl + '#another-page') - await browser.assert.strictEqual(navigationBackSpan.tags['location.href'], startUrl) - - browser.globals.clearReceivedSpans() - await browser.click('#btnGoForward') - - const navigationForwardSpan = await browser.globals.findSpan((span) => span.name === 'routeChange') - await browser.assert.ok(!!navigationForwardSpan, 'Checking forwards navigation span presence.') - - await browser.assert.strictEqual(navigationForwardSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(navigationForwardSpan.tags['prev.href'], startUrl) - await browser.assert.strictEqual(navigationForwardSpan.tags['location.href'], startUrl + '#another-page') - }, -} diff --git a/packages/web/integration-tests/tests/user-interaction/keyboard.spec.js b/packages/web/integration-tests/tests/user-interaction/keyboard.spec.js deleted file mode 100644 index e07cc16a..00000000 --- a/packages/web/integration-tests/tests/user-interaction/keyboard.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'handles keyboard events': async function (browser) { - browser.globals.clearReceivedSpans() - - const startUrl = browser.globals.getUrl('/user-interaction/keyboard.ejs') - await browser.url(startUrl) - await browser.sendKeys('body', 'a') - - const keydownSpan = await browser.globals.findSpan((span) => span.name === 'keydown') - await browser.assert.ok(!!keydownSpan, 'Checking keyboard span presence.') - - await browser.assert.strictEqual(keydownSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(keydownSpan.tags['event_type'], 'keydown') - await browser.assert.strictEqual(keydownSpan.tags['target_element'], 'BODY') - await browser.assert.strictEqual(keydownSpan.tags['target_xpath'], '//html/body') - - const keyupSpan = await browser.globals.findSpan((span) => span.name === 'keyup') - await browser.assert.ok(!!keyupSpan, 'Checking keyboard span presence.') - - await browser.assert.strictEqual(keyupSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(keyupSpan.tags['event_type'], 'keyup') - await browser.assert.strictEqual(keyupSpan.tags['target_element'], 'BODY') - await browser.assert.strictEqual(keyupSpan.tags['target_xpath'], '//html/body') - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/user-interaction/mouse.spec.js b/packages/web/integration-tests/tests/user-interaction/mouse.spec.js deleted file mode 100644 index ff655914..00000000 --- a/packages/web/integration-tests/tests/user-interaction/mouse.spec.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'handles mouse click': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/user-interaction/mouse.ejs')) - await browser.click('#btn1') - - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - await browser.assert.ok(!!clickSpan, 'Checking click span presence.') - - await browser.assert.strictEqual(clickSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(clickSpan.tags['event_type'], 'click') - await browser.assert.strictEqual(clickSpan.tags['target_element'], 'BUTTON') - await browser.assert.strictEqual(clickSpan.tags['target_xpath'], '//*[@id="btn1"]') - - await browser.globals.assertNoErrorSpans() - }, - 'handles mouse down': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/user-interaction/mouse.ejs')) - await browser.click('#btn1') - - const mouseDownSpan = await browser.globals.findSpan((span) => span.name === 'mousedown') - await browser.assert.ok(!!mouseDownSpan, 'Checking mousedown span presence.') - - await browser.assert.strictEqual(mouseDownSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(mouseDownSpan.tags['event_type'], 'mousedown') - await browser.assert.strictEqual(mouseDownSpan.tags['target_element'], 'BUTTON') - await browser.assert.strictEqual(mouseDownSpan.tags['target_xpath'], '//*[@id="btn1"]') - }, - 'handles mouse up': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/user-interaction/mouse.ejs')) - await browser.click('#btn1') - - const mouseUpSpan = await browser.globals.findSpan((span) => span.name === 'mouseup') - await browser.assert.ok(!!mouseUpSpan, 'Checking mouseup span presence.') - - await browser.assert.strictEqual(mouseUpSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(mouseUpSpan.tags['event_type'], 'mouseup') - await browser.assert.strictEqual(mouseUpSpan.tags['target_element'], 'BUTTON') - await browser.assert.strictEqual(mouseUpSpan.tags['target_xpath'], '//*[@id="btn1"]') - }, - 'handles disabling of mouse events': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/user-interaction/mouse-disabled.ejs')) - await browser.click('#btn1') - await browser.click('#btnGuard') - - const guardSpan = await browser.globals.findSpan((span) => span.name === 'guard') - await browser.assert.ok(guardSpan, 'Checking presence of guard span.') - - await browser.assert.not.ok( - browser.globals.getReceivedSpans().find((span) => span.name === 'mouseup'), - 'Ensuring no mouseup span arrived.', - ) - await browser.assert.not.ok( - browser.globals.getReceivedSpans().find((span) => span.name === 'mousedown'), - 'Ensuring no mousedown span arrived.', - ) - await browser.assert.not.ok( - browser.globals.getReceivedSpans().find((span) => span.name === 'click'), - 'Ensuring no click span arrived.', - ) - }, - 'handles clicks with event listener on document': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/user-interaction/mouse-document.ejs')) - await browser.click('#btn1') - - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - await browser.assert.ok(!!clickSpan, 'Checking click span presence.') - - await browser.assert.strictEqual(clickSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(clickSpan.tags['event_type'], 'click') - await browser.assert.strictEqual(clickSpan.tags['target_element'], 'BUTTON') - await browser.assert.strictEqual(clickSpan.tags['target_xpath'], '//*[@id="btn1"]') - - await browser.globals.assertNoErrorSpans() - }, - 'this is bubbled event is correct (open-telemetry/opentelemetry-js-contrib#643)': async function (browser) { - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/user-interaction/mouse-bubble.ejs')) - await browser.click('#inner') - - await browser.globals.findSpan((span) => span.name === 'click') - await browser.expect.element('#result').text.to.equal('container') - - await browser.globals.assertNoErrorSpans() - }, - 'handles svg interactions': async function (browser) { - browser.globals.clearReceivedSpans() - - await browser.url(browser.globals.getUrl('/user-interaction/mouse.ejs')) - await browser.click('#btn-svg-target') - - const clickSpan = await browser.globals.findSpan((span) => span.name === 'click') - await browser.assert.ok(!!clickSpan, 'Checking click span presence.') - - await browser.assert.strictEqual(clickSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(clickSpan.tags['event_type'], 'click') - await browser.assert.strictEqual(clickSpan.tags['target_element'], 'rect') - await browser.assert.strictEqual(clickSpan.tags['target_xpath'], '//*[@id="btn-svg-target"]') - - const mouseDownSpan = await browser.globals.findSpan((span) => span.name === 'mousedown') - await browser.assert.ok(!!mouseDownSpan, 'Checking mousedown span presence.') - - await browser.assert.strictEqual(mouseDownSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(mouseDownSpan.tags['event_type'], 'mousedown') - await browser.assert.strictEqual(mouseDownSpan.tags['target_element'], 'rect') - await browser.assert.strictEqual(mouseDownSpan.tags['target_xpath'], '//*[@id="btn-svg-target"]') - - const mouseUpSpan = await browser.globals.findSpan((span) => span.name === 'mouseup') - await browser.assert.ok(!!mouseUpSpan, 'Checking mouseup span presence.') - - await browser.assert.strictEqual(mouseUpSpan.tags['component'], 'user-interaction') - await browser.assert.strictEqual(mouseUpSpan.tags['event_type'], 'mouseup') - await browser.assert.strictEqual(mouseUpSpan.tags['target_element'], 'rect') - await browser.assert.strictEqual(mouseUpSpan.tags['target_xpath'], '//*[@id="btn-svg-target"]') - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/visibility/visibility.spec.js b/packages/web/integration-tests/tests/visibility/visibility.spec.js deleted file mode 100644 index ab9c7c8a..00000000 --- a/packages/web/integration-tests/tests/visibility/visibility.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'visibility events are captured': async function (browser) { - await browser.url(browser.globals.getUrl('/visibility/visibility.ejs')) - browser.globals.emulateTabSwitching(true) - - const tabHiddenSpan = await browser.globals.findSpan( - (span) => span.name === 'visibility' && span.tags['hidden'] === 'true', - ) - const hiddenSpans = browser.globals.getReceivedSpans().filter((span) => span.name === 'visibility') - await browser.assert.ok(!!tabHiddenSpan, 'Visibility event for hidden tab exists') - await browser.assert.strictEqual(hiddenSpans.length, 1, 'There is only one visibility event') - - browser.globals.clearReceivedSpans() - - browser.globals.emulateTabSwitching(false) - const tabVisibileSpan = await browser.globals.findSpan( - (span) => span.name === 'visibility' && span.tags['hidden'] === 'false', - ) - const visibleSpans = browser.globals.getReceivedSpans().filter((span) => span.name === 'visibility') - await browser.assert.ok(!!tabVisibileSpan, 'Visibility event for visible tab exists') - await browser.assert.strictEqual(visibleSpans.length, 1, 'There is only one visibility event') - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/webvitals/webvitals.spec.js b/packages/web/integration-tests/tests/webvitals/webvitals.spec.js deleted file mode 100644 index 1cac58f2..00000000 --- a/packages/web/integration-tests/tests/webvitals/webvitals.spec.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -module.exports = { - 'webvitals spans': async function (browser) { - // the google webvitals library only works on chrome (and chrome-based edge) at the moment - if (!browser.globals.isBrowser({ chrome: true, microsoftedge: true })) { - return - } - - const url = browser.globals.getUrl('/webvitals/webvitals.ejs') - await browser.url(url) - // fid won't happen unless there is interaction, so simulate a bit of that - await browser.click('#clicky') - await browser.waitForElementPresent('#p2') - // webvitals library won't send the cls unless a visibility change happens, so - // force a fake one - await browser.execute(function () { - Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true }) - document.dispatchEvent(new Event('visibilitychange')) - }) - // force a sync - await browser.globals.waitForTestToFinish() - - const fid = await browser.globals.findSpan((span) => span.tags.fid !== undefined) - const inp = await browser.globals.findSpan((span) => span.tags.inp !== undefined) - - browser.assert.ok( - !!browser.globals.getReceivedSpans().find((span) => span.tags.lcp !== undefined), - 'Checking presence of lcp span.', - ) - browser.assert.ok( - !!browser.globals.getReceivedSpans().find((span) => span.tags.cls !== undefined), - 'Checking presence of cls span.', - ) - - // sometimes missing in automated tests - if (fid) { - await browser.assert.strictEqual(fid.name, 'webvitals') - await browser.assert.ok(fid.tags.fid > 0) - } - - if (inp) { - await browser.assert.strictEqual(inp.name, 'webvitals') - } - - await browser.globals.assertNoErrorSpans() - }, - 'webvitals - specific metrics disabled': async function (browser) { - // the google webvitals library only works on chrome (and chrome-based edge) at the moment - if (!browser.globals.isBrowser({ chrome: true, microsoftedge: true })) { - return - } - - const url = browser.globals.getUrl('/webvitals/webvitals-specific-disabled.ejs') - await browser.url(url) - // fid won't happen unless there is interaction, so simulate a bit of that - await browser.click('#clicky') - await browser.waitForElementPresent('#p2') - // webvitals library won't send the cls unless a visibility change happens, so - // force a fake one - await browser.execute(function () { - Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true }) - document.dispatchEvent(new Event('visibilitychange')) - }) - // force a sync - await browser.globals.waitForTestToFinish() - - const lcp = await browser.globals.findSpan((span) => span.tags.lcp !== undefined) - const cls = await browser.globals.findSpan((span) => span.tags.cls !== undefined) - const fid = await browser.globals.findSpan((span) => span.tags.fid !== undefined) - const inp = await browser.globals.findSpan((span) => span.tags.inp !== undefined) - - await browser.assert.ok(lcp) - await browser.assert.ok(cls) - - await browser.assert.strictEqual(lcp.name, 'webvitals') - await browser.assert.strictEqual(cls.name, 'webvitals') - - await browser.assert.ok(lcp.tags.lcp > 0) - await browser.assert.ok(cls.tags.cls >= 0) - - // sometimes missing in automated tests - if (fid) { - await browser.assert.strictEqual(fid.name, 'webvitals') - await browser.assert.ok(fid.tags.fid > 0) - } - - if (inp) { - await browser.assert.strictEqual(inp.name, 'webvitals') - } - - await browser.globals.assertNoErrorSpans() - }, -} diff --git a/packages/web/integration-tests/tests/xhr/xhr-basic.spec.js b/packages/web/integration-tests/tests/xhr/xhr-basic.spec.js deleted file mode 100644 index 408a9a9b..00000000 --- a/packages/web/integration-tests/tests/xhr/xhr-basic.spec.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * - * Copyright 2024 Splunk Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const { isBrowser } = require('../../utils/helpers') - -module.exports = { - '@tags': ['safari-10.1'], - 'beforeEach': function (browser) { - browser.globals.clearReceivedSpans() - }, - 'XHR request is registered': async function (browser) { - await browser.url(browser.globals.getUrl('/xhr/views/xhr-basic.ejs')) - const xhrSpan = await browser.globals.findSpan( - (span) => span.tags['http.url'] === browser.globals.getUrl('/some-data', []), - ) - await browser.assert.ok(xhrSpan, 'got an xhr span') - await browser.assert.strictEqual(xhrSpan.tags['component'], 'xml-http-request') - await browser.assert.strictEqual(xhrSpan.tags['http.status_code'], '200') - await browser.assert.strictEqual(xhrSpan.tags['http.status_text'], 'OK') - await browser.assert.strictEqual(xhrSpan.tags['http.method'], 'GET') - await browser.assert.strictEqual(xhrSpan.tags['http.url'], browser.globals.getUrl('/some-data', [])) - if (!isBrowser(browser, { safari: true, ie: true })) { - await browser.assert.strictEqual(xhrSpan.tags['http.response_content_length'], '49') - } - - await browser.assert.ok(xhrSpan.tags['link.traceId'], 'got link.traceId') - await browser.assert.ok(xhrSpan.tags['link.spanId'], 'got link.spanId') - - if ( - !isBrowser(browser, { - safari: { max: 10 }, - ie: true, - }) - ) { - await browser.timesMakeSense(xhrSpan.annotations, 'domainLookupStart', 'domainLookupEnd') - await browser.timesMakeSense(xhrSpan.annotations, 'connectStart', 'connectEnd') - if (browser.globals.enableHttps) { - await browser.timesMakeSense(xhrSpan.annotations, 'secureConnectionStart', 'connectEnd') - } - - await browser.timesMakeSense(xhrSpan.annotations, 'requestStart', 'responseStart') - await browser.timesMakeSense(xhrSpan.annotations, 'responseStart', 'responseEnd') - await browser.timesMakeSense(xhrSpan.annotations, 'fetchStart', 'responseEnd') - } - - await browser.timesMakeSense(xhrSpan.annotations, 'open', 'send') - await browser.timesMakeSense(xhrSpan.annotations, 'send', 'loaded') - await browser.globals.assertNoErrorSpans() - }, - 'module can be disabled': async function (browser) { - await browser.url(browser.globals.getUrl('/xhr/views/xhr-basic.ejs')) - await browser.globals.waitForTestToFinish() - - const awaitedUrl = browser.globals.getUrl('/some-data', []) - browser.assert.ok( - !!browser.globals.getReceivedSpans().find((span) => span.tags['http.url'] === awaitedUrl), - 'Checking presence of xhr span.', - ) - - browser.globals.clearReceivedSpans() - await browser.url(browser.globals.getUrl('/xhr/views/xhr-basic.ejs?disableInstrumentation=xhr')) - await browser.globals.waitForTestToFinish() - - browser.assert.not.ok( - browser.globals.getReceivedSpans().find((span) => span.tags['http.url'] === awaitedUrl), - 'Checking lack of xhr span.', - ) - }, -}