diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..a52d67d2 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,67 @@ +# E2E Tests for analytics-react-native +# Copy this file to: analytics-react-native/.github/workflows/e2e-tests.yml +# +# This workflow: +# 1. Checks out the SDK and sdk-e2e-tests repos +# 2. Builds the Node-based e2e-cli +# 3. Runs the e2e test suite + +name: E2E Tests + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: # Allow manual trigger + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout SDK + uses: actions/checkout@v4 + with: + path: sdk + + - name: Checkout sdk-e2e-tests + uses: actions/checkout@v4 + with: + repository: segmentio/sdk-e2e-tests + path: sdk-e2e-tests + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install and build e2e-cli + working-directory: sdk/e2e-cli + run: | + npm install + npm run build + + - name: Install sdk-e2e-tests dependencies + working-directory: sdk-e2e-tests + run: npm ci + + - name: Build sdk-e2e-tests + working-directory: sdk-e2e-tests + run: npm run build + + - name: Run E2E tests + working-directory: sdk-e2e-tests + env: + CLI_COMMAND: node ${{ github.workspace }}/sdk/e2e-cli/dist/cli.js + E2E_TEST_SUITES: basic,retry + # E2E_TEST_SKIP: exponential-backoff # skip specific test files if needed + run: npm test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-test-results + path: sdk-e2e-tests/test-results/ + if-no-files-found: ignore diff --git a/e2e-cli/.gitignore b/e2e-cli/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/e2e-cli/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/e2e-cli/README.md b/e2e-cli/README.md new file mode 100644 index 00000000..93771f4e --- /dev/null +++ b/e2e-cli/README.md @@ -0,0 +1,54 @@ +# analytics-react-native e2e-cli + +E2E test CLI for the [@segment/analytics-react-native](https://github.com/segmentio/analytics-react-native) SDK. Runs the **real SDK pipeline** on Node.js — events flow through `SegmentClient` → Timeline → `SegmentDestination` (batch chunking, upload) → `QueueFlushingPlugin` (queue management) → `uploadEvents` HTTP POST. + +React Native runtime dependencies (`AppState`, `NativeModules`, sovran native bridge, AsyncStorage) are stubbed with minimal Node.js equivalents so the full event processing pipeline executes without a React Native runtime. + +## Setup + +```bash +npm install +npm run build +``` + +The build uses esbuild to bundle the CLI + SDK source + stubs into a single `dist/cli.js`. + +## Usage + +```bash +node dist/cli.js --input '{"writeKey":"...", ...}' +``` + +## Input Format + +```jsonc +{ + "writeKey": "your-write-key", // required + "apiHost": "https://...", // optional — SDK default if omitted + "cdnHost": "https://...", // optional — SDK default if omitted + "sequences": [ // required — event sequences to send + { + "delayMs": 0, + "events": [ + { "type": "track", "event": "Test", "userId": "user-1" } + ] + } + ], + "config": { // optional + "flushAt": 20, + "flushInterval": 30 + } +} +``` + +## Output Format + +```json +{ "success": true } +``` + +On failure: + +```json +{ "success": false, "error": "description" } +``` diff --git a/e2e-cli/build.js b/e2e-cli/build.js new file mode 100644 index 00000000..16dbcca6 --- /dev/null +++ b/e2e-cli/build.js @@ -0,0 +1,35 @@ +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const esbuild = require('esbuild'); + +const coreDir = path.resolve(__dirname, '../packages/core'); +const infoFile = path.join(coreDir, 'src/info.ts'); + +// Generate info.ts if it doesn't exist (required by context.ts) +if (!fs.existsSync(infoFile)) { + console.log('Generating packages/core/src/info.ts...'); + execSync('node constants-generator.js', { cwd: coreDir, stdio: 'inherit' }); +} + +esbuild.buildSync({ + entryPoints: [path.resolve(__dirname, 'src/cli.ts')], + bundle: true, + platform: 'node', + target: 'node18', + format: 'cjs', + outfile: path.resolve(__dirname, 'dist/cli.js'), + alias: { + 'react-native': path.resolve(__dirname, 'src/stubs/react-native.ts'), + '@segment/sovran-react-native': path.resolve( + __dirname, + 'src/stubs/sovran.ts' + ), + 'react-native-get-random-values': path.resolve( + __dirname, + 'src/stubs/react-native-get-random-values.ts' + ), + }, + external: ['uuid', 'deepmerge', '@react-native-async-storage/async-storage'], + logLevel: 'info', +}); diff --git a/e2e-cli/package-lock.json b/e2e-cli/package-lock.json new file mode 100644 index 00000000..a8194085 --- /dev/null +++ b/e2e-cli/package-lock.json @@ -0,0 +1,512 @@ +{ + "name": "@segment/analytics-react-native-e2e-cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@segment/analytics-react-native-e2e-cli", + "version": "1.0.0", + "dependencies": { + "deepmerge": "^4.3.1", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@types/uuid": "^10.0.0", + "esbuild": "^0.20.0", + "typescript": "^5.2.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/e2e-cli/package.json b/e2e-cli/package.json new file mode 100644 index 00000000..14a5ab53 --- /dev/null +++ b/e2e-cli/package.json @@ -0,0 +1,21 @@ +{ + "name": "@segment/analytics-react-native-e2e-cli", + "version": "1.0.0", + "private": true, + "description": "E2E CLI for React Native analytics SDK — runs the real SDK pipeline on Node.js", + "main": "dist/cli.js", + "scripts": { + "build": "node build.js", + "start": "node dist/cli.js" + }, + "dependencies": { + "deepmerge": "^4.3.1", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@types/uuid": "^10.0.0", + "esbuild": "^0.20.0", + "typescript": "^5.2.2" + } +} diff --git a/e2e-cli/src/cli.ts b/e2e-cli/src/cli.ts new file mode 100644 index 00000000..9efdbc5b --- /dev/null +++ b/e2e-cli/src/cli.ts @@ -0,0 +1,185 @@ +/** + * E2E CLI for React Native analytics SDK testing + * + * Runs the real SDK pipeline (SegmentClient → Timeline → SegmentDestination → + * QueueFlushingPlugin → uploadEvents) with stubs for React Native runtime + * dependencies so everything executes on Node.js. + * + * Usage: + * node dist/cli.js --input '{"writeKey":"...", ...}' + */ + +import { SegmentClient } from '../../packages/core/src/analytics'; +import { SovranStorage } from '../../packages/core/src/storage/sovranStorage'; +import { Logger } from '../../packages/core/src/logger'; +import type { Config, JsonMap } from '../../packages/core/src/types'; +import type { Persistor } from '@segment/sovran-react-native'; + +// ============================================================================ +// CLI Input/Output Types +// ============================================================================ + +interface CLIInput { + writeKey: string; + apiHost?: string; + cdnHost?: string; + sequences: Array<{ + delayMs: number; + events: Array<{ + type: string; + event?: string; + userId?: string; + properties?: Record; + traits?: Record; + }>; + }>; + config?: { + flushAt?: number; + flushInterval?: number; + }; +} + +interface CLIOutput { + success: boolean; + error?: string; +} + +// ============================================================================ +// In-memory Persistor for Node.js (replaces AsyncStorage) +// ============================================================================ + +const memStore = new Map(); +const MemoryPersistor: Persistor = { + get: async (key: string): Promise => + memStore.get(key) as T | undefined, + set: async (key: string, state: T): Promise => { + memStore.set(key, state); + }, +}; + +// ============================================================================ +// Main CLI Logic +// ============================================================================ + +async function main() { + const args = process.argv.slice(2); + let inputStr: string | undefined; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--input' && i + 1 < args.length) { + inputStr = args[i + 1]; + break; + } + } + + if (!inputStr) { + console.log(JSON.stringify({ success: false, error: 'No input provided' })); + process.exit(1); + } + + let output: CLIOutput; + + try { + const input: CLIInput = JSON.parse(inputStr); + + // Build SDK config + const config: Config = { + writeKey: input.writeKey, + trackAppLifecycleEvents: false, + trackDeepLinks: false, + autoAddSegmentDestination: true, + storePersistor: MemoryPersistor, + storePersistorSaveDelay: 0, + // When apiHost is provided (mock tests), use proxy to direct events there + ...(input.apiHost && { + proxy: input.apiHost, + useSegmentEndpoints: true, + }), + // Provide default settings so SDK doesn't require CDN response + defaultSettings: { + integrations: { + 'Segment.io': { + apiKey: input.writeKey, + apiHost: 'api.segment.io/v1', + }, + }, + }, + ...(input.config?.flushAt !== undefined && { + flushAt: input.config.flushAt, + }), + ...(input.config?.flushInterval !== undefined && { + flushInterval: input.config.flushInterval, + }), + }; + + // Create storage with in-memory persistor + const store = new SovranStorage({ + storeId: input.writeKey, + storePersistor: MemoryPersistor, + storePersistorSaveDelay: 0, + }); + + // Create client with logging disabled (suppress SDK internal logs) + const logger = new Logger(true); + const client = new SegmentClient({ config, logger, store }); + + // Initialize — adds plugins, resolves settings, processes pending events + await client.init(); + + // Process event sequences + for (const sequence of input.sequences) { + if (sequence.delayMs > 0) { + await new Promise((resolve) => setTimeout(resolve, sequence.delayMs)); + } + + for (const evt of sequence.events) { + switch (evt.type) { + case 'track': + await client.track( + evt.event!, + evt.properties as JsonMap | undefined + ); + break; + case 'identify': + await client.identify(evt.userId, evt.traits as JsonMap | undefined); + break; + case 'screen': + await client.screen( + evt.event!, + evt.properties as JsonMap | undefined + ); + break; + case 'group': + await client.group( + evt.event!, + evt.traits as JsonMap | undefined + ); + break; + case 'alias': + await client.alias(evt.userId!); + break; + } + } + } + + // Flush all queued events through the real pipeline + await client.flush(); + + // Brief delay to let async upload operations settle + await new Promise((resolve) => setTimeout(resolve, 500)); + + client.cleanup(); + + output = { success: true }; + } catch (e) { + const error = e instanceof Error ? e.message : String(e); + output = { success: false, error }; + } + + console.log(JSON.stringify(output)); +} + +main().catch((e) => { + console.log(JSON.stringify({ success: false, error: String(e) })); + process.exit(1); +}); diff --git a/e2e-cli/src/stubs/react-native-get-random-values.ts b/e2e-cli/src/stubs/react-native-get-random-values.ts new file mode 100644 index 00000000..0a6c0334 --- /dev/null +++ b/e2e-cli/src/stubs/react-native-get-random-values.ts @@ -0,0 +1,2 @@ +// No-op — Node.js 18+ has native crypto.getRandomValues() +export {}; diff --git a/e2e-cli/src/stubs/react-native.ts b/e2e-cli/src/stubs/react-native.ts new file mode 100644 index 00000000..5dc83025 --- /dev/null +++ b/e2e-cli/src/stubs/react-native.ts @@ -0,0 +1,52 @@ +/** + * Stub for react-native — provides the minimal surface needed by the SDK + * to run on Node.js without the React Native runtime. + */ + +export type AppStateStatus = string; + +export interface NativeEventSubscription { + remove: () => void; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface NativeModule {} + +export const AppState = { + currentState: 'active' as AppStateStatus, + addEventListener: ( + _type: string, + _handler: (state: AppStateStatus) => void + ): NativeEventSubscription => ({ + remove: () => {}, + }), +}; + +export const NativeModules: Record = { + AnalyticsReactNative: { + getContextInfo: async () => ({ + appName: 'e2e-cli', + appVersion: '1.0.0', + buildNumber: '1', + bundleId: 'com.segment.e2ecli', + locale: 'en-US', + networkType: 'wifi', + osName: 'Node.js', + osVersion: process.version, + screenHeight: 0, + screenWidth: 0, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + manufacturer: '', + model: '', + deviceName: 'e2e-cli', + deviceId: '', + deviceType: 'node', + screenDensity: 0, + }), + }, +}; + +export const Platform = { + OS: 'node', + select: (opts: Record): string => opts.default ?? '', +}; diff --git a/e2e-cli/src/stubs/sovran.ts b/e2e-cli/src/stubs/sovran.ts new file mode 100644 index 00000000..363ef774 --- /dev/null +++ b/e2e-cli/src/stubs/sovran.ts @@ -0,0 +1,13 @@ +/** + * Stub for @segment/sovran-react-native — re-exports the real pure-TS + * implementations, bypassing index.tsx which has React Native bridge deps. + */ + +export { createStore } from '../../../packages/sovran/src/store'; +export type { Store, Notify, Unsubscribe } from '../../../packages/sovran/src/store'; +export { registerBridgeStore } from '../../../packages/sovran/src/bridge'; +export type { + Persistor, + PersistenceConfig, +} from '../../../packages/sovran/src/persistor/persistor'; +export { AsyncStoragePersistor } from '../../../packages/sovran/src/persistor/async-storage-persistor'; diff --git a/e2e-cli/tsconfig.json b/e2e-cli/tsconfig.json new file mode 100644 index 00000000..c0c3118d --- /dev/null +++ b/e2e-cli/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "paths": { + "react-native": ["./src/stubs/react-native"], + "@segment/sovran-react-native": ["./src/stubs/sovran"], + "react-native-get-random-values": [ + "./src/stubs/react-native-get-random-values" + ] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}