From a23de9e09cae61ddc6fff577b07ead330d5f5df5 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 9 Feb 2026 12:05:24 -0500 Subject: [PATCH 1/4] Add e2e-cli and e2e-tests workflow Node-based CLI that copies SDK upload/error-handling logic for e2e testing against mock server. Includes E2E_TEST_SUITES for selective test execution. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/e2e-tests.yml | 67 ++++++++ e2e-cli/package-lock.json | 71 +++++++++ e2e-cli/package.json | 19 +++ e2e-cli/src/cli.ts | 260 ++++++++++++++++++++++++++++++++ e2e-cli/tsconfig.json | 16 ++ 5 files changed, 433 insertions(+) create mode 100644 .github/workflows/e2e-tests.yml create mode 100644 e2e-cli/package-lock.json create mode 100644 e2e-cli/package.json create mode 100644 e2e-cli/src/cli.ts create mode 100644 e2e-cli/tsconfig.json 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/package-lock.json b/e2e-cli/package-lock.json new file mode 100644 index 00000000..8b4789e7 --- /dev/null +++ b/e2e-cli/package-lock.json @@ -0,0 +1,71 @@ +{ + "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": { + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@types/uuid": "^10.0.0", + "typescript": "^5.2.2" + } + }, + "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/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..9a9eaec7 --- /dev/null +++ b/e2e-cli/package.json @@ -0,0 +1,19 @@ +{ + "name": "@segment/analytics-react-native-e2e-cli", + "version": "1.0.0", + "private": true, + "description": "E2E CLI for React Native analytics SDK testing", + "main": "dist/cli.js", + "scripts": { + "build": "tsc", + "start": "node dist/cli.js" + }, + "dependencies": { + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@types/uuid": "^10.0.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..b45e7835 --- /dev/null +++ b/e2e-cli/src/cli.ts @@ -0,0 +1,260 @@ +/** + * E2E CLI for React Native analytics SDK testing + * + * This CLI uses code copied directly from the SDK's api.ts and errors.ts + * to test the real HTTP upload and error handling behavior. + * + * Usage: + * node dist/cli.js --input '{"writeKey":"...", ...}' + */ + +import { v4 as uuidv4 } from "uuid"; + +// ============================================================================ +// Types (from SDK's types.ts) +// ============================================================================ + +interface SegmentEvent { + type: string; + anonymousId: string; + timestamp: string; + messageId: string; + context?: Record; + userId?: string; + event?: string; + properties?: Record; + traits?: Record; +} + +// ============================================================================ +// Copied from SDK's util.ts - getURL and validateURL +// ============================================================================ + +function getURL(host: string, path: string): string { + if (!host.startsWith("https://") && !host.startsWith("http://")) { + host = "https://" + host; + } + const s = `${host}${path}`; + if (!validateURL(s)) { + console.error("Invalid URL has been passed"); + console.log(`Invalid Url passed is ${s}`); + throw new Error("Invalid URL has been passed"); + } + return s; +} + +function validateURL(url: string): boolean { + const urlRegex = new RegExp( + "^(?:https?:\\/\\/)" + // Protocol (http or https) + "(?:\\S+(?::\\S*)?@)?" + // Optional user:pass@ + "(?:(localhost|\\d{1,3}(?:\\.\\d{1,3}){3})|" + // Localhost or IP address + "(?:(?!-)[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*(?:\\.[a-zA-Z]{2,})))" + // Domain validation (supports hyphens) + "(?::\\d{2,5})?" + // Optional port + "(\\/[^\\s?#]*)?" + // Path (allows `/projects/yup/settings`) + "(\\?[a-zA-Z0-9_.-]+=[a-zA-Z0-9_.-]+(&[a-zA-Z0-9_.-]+=[a-zA-Z0-9_.-]+)*)?" + // Query params + "(#[^\\s]*)?$", // Fragment (optional) + "i" // Case-insensitive + ); + return urlRegex.test(url); +} + +// ============================================================================ +// Copied from SDK's api.ts - uploadEvents +// ============================================================================ + +const uploadEvents = async ({ + writeKey, + url, + events, +}: { + writeKey: string; + url: string; + events: SegmentEvent[]; +}) => { + return await fetch(url, { + method: "POST", + body: JSON.stringify({ + batch: events, + sentAt: new Date().toISOString(), + writeKey: writeKey, + }), + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }); +}; + +// ============================================================================ +// Copied from SDK's errors.ts - checkResponseForErrors +// ============================================================================ + +enum ErrorType { + NetworkUnexpectedHTTPCode, + NetworkServerLimited, + NetworkServerRejected, + NetworkUnknown, +} + +class SegmentError extends Error { + type: ErrorType; + message: string; + innerError?: unknown; + + constructor(type: ErrorType, message: string, innerError?: unknown) { + super(message); + Object.setPrototypeOf(this, SegmentError.prototype); + this.type = type; + this.message = message; + this.innerError = innerError; + } +} + +class NetworkError extends SegmentError { + statusCode: number; + type: + | ErrorType.NetworkServerLimited + | ErrorType.NetworkServerRejected + | ErrorType.NetworkUnexpectedHTTPCode + | ErrorType.NetworkUnknown; + + constructor(statusCode: number, message: string, innerError?: unknown) { + let type: ErrorType; + if (statusCode === 429) { + type = ErrorType.NetworkServerLimited; + } else if (statusCode > 300 && statusCode < 400) { + type = ErrorType.NetworkUnexpectedHTTPCode; + } else if (statusCode >= 400) { + type = ErrorType.NetworkServerRejected; + } else { + type = ErrorType.NetworkUnknown; + } + + super(type, message, innerError); + Object.setPrototypeOf(this, NetworkError.prototype); + + this.statusCode = statusCode; + this.type = type as NetworkError["type"]; + } +} + +const checkResponseForErrors = (response: Response) => { + if (!response.ok) { + throw new NetworkError(response.status, response.statusText); + } + return response; +}; + +// ============================================================================ +// CLI Input/Output Types +// ============================================================================ + +interface CLIInput { + writeKey: string; + apiHost: 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; +} + +// ============================================================================ +// Main CLI Logic +// ============================================================================ + +function createEvent( + input: CLIInput["sequences"][0]["events"][0], + anonymousId: string +): SegmentEvent { + return { + type: input.type, + anonymousId, + timestamp: new Date().toISOString(), + messageId: uuidv4(), + context: { + library: { name: "analytics-react-native", version: "e2e-cli" }, + }, + ...(input.userId && { userId: input.userId }), + ...(input.event && { event: input.event }), + ...(input.properties && { properties: input.properties }), + ...(input.traits && { traits: input.traits }), + }; +} + +async function main() { + // Parse command line arguments + 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.error('Usage: cli --input \'{"writeKey":"...", ...}\''); + console.log(JSON.stringify({ success: false, error: "No input provided" })); + process.exit(1); + } + + let output: CLIOutput; + + try { + const input: CLIInput = JSON.parse(inputStr); + const anonymousId = uuidv4(); + + // Build URL - uses SDK's getURL function + const url = getURL(input.apiHost, "/b"); + + // Collect events from all sequences + const allEvents: SegmentEvent[] = []; + + for (const sequence of input.sequences) { + if (sequence.delayMs > 0) { + await new Promise((resolve) => setTimeout(resolve, sequence.delayMs)); + } + + for (const eventInput of sequence.events) { + allEvents.push(createEvent(eventInput, anonymousId)); + } + } + + // Upload batch - uses SDK's uploadEvents and checkResponseForErrors + if (allEvents.length > 0) { + const response = await uploadEvents({ + writeKey: input.writeKey, + url, + events: allEvents, + }); + checkResponseForErrors(response); + } + + 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/tsconfig.json b/e2e-cli/tsconfig.json new file mode 100644 index 00000000..e180e79f --- /dev/null +++ b/e2e-cli/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From d9a5cb35fd75a26dd5f7b857081f652a277126f7 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 9 Feb 2026 14:48:41 -0500 Subject: [PATCH 2/4] Add .gitignore for e2e-cli build artifacts Co-Authored-By: Claude Opus 4.6 --- e2e-cli/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 e2e-cli/.gitignore 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/ From e91a1cd69074a48d23d9c93e50f576316c6df65f Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 9 Feb 2026 18:21:43 -0500 Subject: [PATCH 3/4] Separate cdnHost from apiHost in e2e-cli, add README Make apiHost and cdnHost optional in the CLI input type contract. Add e2e-cli README documenting the input/output format and parameters. Co-Authored-By: Claude Opus 4.6 --- e2e-cli/README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++ e2e-cli/src/cli.ts | 7 ++++--- 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 e2e-cli/README.md diff --git a/e2e-cli/README.md b/e2e-cli/README.md new file mode 100644 index 00000000..6b0b77de --- /dev/null +++ b/e2e-cli/README.md @@ -0,0 +1,50 @@ +# analytics-react-native e2e-cli + +E2E test CLI for the [@segment/analytics-react-native](https://github.com/segmentio/analytics-react-native) SDK. This CLI uses code copied from the SDK's internals (HTTP upload, error handling) to test the real network behavior from Node.js, bypassing the React Native runtime. + +## Setup + +```bash +npm install +npm run build +``` + +## Usage + +```bash +node dist/cli.js --input '{"writeKey":"...", ...}' +``` + +## Input Format + +```jsonc +{ + "writeKey": "your-write-key", // required + "apiHost": "https://...", // optional — defaults to api.segment.io + "cdnHost": "https://...", // optional — not used yet (reserved for future) + "sequences": [ // required — event sequences to send + { + "delayMs": 0, + "events": [ + { "type": "track", "event": "Test", "userId": "user-1" } + ] + } + ], + "config": { // optional + "flushAt": 1, + "flushInterval": 1000 + } +} +``` + +## Output Format + +```json +{ "success": true } +``` + +On failure: + +```json +{ "success": false, "error": "description" } +``` diff --git a/e2e-cli/src/cli.ts b/e2e-cli/src/cli.ts index b45e7835..12b755d0 100644 --- a/e2e-cli/src/cli.ts +++ b/e2e-cli/src/cli.ts @@ -150,7 +150,8 @@ const checkResponseForErrors = (response: Response) => { interface CLIInput { writeKey: string; - apiHost: string; + apiHost?: string; + cdnHost?: string; sequences: Array<{ delayMs: number; events: Array<{ @@ -219,8 +220,8 @@ async function main() { const input: CLIInput = JSON.parse(inputStr); const anonymousId = uuidv4(); - // Build URL - uses SDK's getURL function - const url = getURL(input.apiHost, "/b"); + // Build URL - uses SDK's getURL function (default matches SDK's internal default) + const url = getURL(input.apiHost ?? "api.segment.io", "/b"); // Collect events from all sequences const allEvents: SegmentEvent[] = []; From dffb3eb3b962baa5163409141f97f1b11deae797 Mon Sep 17 00:00:00 2001 From: Michael Grosse Huelsewiesche Date: Mon, 9 Feb 2026 20:49:59 -0500 Subject: [PATCH 4/4] Rewrite e2e-cli to run real SDK pipeline with Node.js stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace copied SDK functions with real imports from the SDK source. Events now flow through the full pipeline: SegmentClient → Timeline → SegmentDestination (batch chunking) → QueueFlushingPlugin (queue) → uploadEvents HTTP POST. Three minimal stubs replace React Native runtime dependencies: - react-native: mocks AppState, NativeModules, Platform - @segment/sovran-react-native: re-exports real store.ts + bridge.ts, bypassing the RN bridge entry point - react-native-get-random-values: no-op (Node.js has native crypto) Uses esbuild to bundle CLI + SDK source + stubs into dist/cli.js. Co-Authored-By: Claude Opus 4.6 --- e2e-cli/README.md | 14 +- e2e-cli/build.js | 35 ++ e2e-cli/package-lock.json | 441 ++++++++++++++++++ e2e-cli/package.json | 6 +- e2e-cli/src/cli.ts | 280 ++++------- .../stubs/react-native-get-random-values.ts | 2 + e2e-cli/src/stubs/react-native.ts | 52 +++ e2e-cli/src/stubs/sovran.ts | 13 + e2e-cli/tsconfig.json | 11 +- 9 files changed, 666 insertions(+), 188 deletions(-) create mode 100644 e2e-cli/build.js create mode 100644 e2e-cli/src/stubs/react-native-get-random-values.ts create mode 100644 e2e-cli/src/stubs/react-native.ts create mode 100644 e2e-cli/src/stubs/sovran.ts diff --git a/e2e-cli/README.md b/e2e-cli/README.md index 6b0b77de..93771f4e 100644 --- a/e2e-cli/README.md +++ b/e2e-cli/README.md @@ -1,6 +1,8 @@ # analytics-react-native e2e-cli -E2E test CLI for the [@segment/analytics-react-native](https://github.com/segmentio/analytics-react-native) SDK. This CLI uses code copied from the SDK's internals (HTTP upload, error handling) to test the real network behavior from Node.js, bypassing the React Native runtime. +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 @@ -9,6 +11,8 @@ npm install npm run build ``` +The build uses esbuild to bundle the CLI + SDK source + stubs into a single `dist/cli.js`. + ## Usage ```bash @@ -20,8 +24,8 @@ node dist/cli.js --input '{"writeKey":"...", ...}' ```jsonc { "writeKey": "your-write-key", // required - "apiHost": "https://...", // optional — defaults to api.segment.io - "cdnHost": "https://...", // optional — not used yet (reserved for future) + "apiHost": "https://...", // optional — SDK default if omitted + "cdnHost": "https://...", // optional — SDK default if omitted "sequences": [ // required — event sequences to send { "delayMs": 0, @@ -31,8 +35,8 @@ node dist/cli.js --input '{"writeKey":"...", ...}' } ], "config": { // optional - "flushAt": 1, - "flushInterval": 1000 + "flushAt": 20, + "flushInterval": 30 } } ``` 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 index 8b4789e7..a8194085 100644 --- a/e2e-cli/package-lock.json +++ b/e2e-cli/package-lock.json @@ -8,14 +8,407 @@ "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", @@ -33,6 +426,54 @@ "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", diff --git a/e2e-cli/package.json b/e2e-cli/package.json index 9a9eaec7..14a5ab53 100644 --- a/e2e-cli/package.json +++ b/e2e-cli/package.json @@ -2,18 +2,20 @@ "name": "@segment/analytics-react-native-e2e-cli", "version": "1.0.0", "private": true, - "description": "E2E CLI for React Native analytics SDK testing", + "description": "E2E CLI for React Native analytics SDK — runs the real SDK pipeline on Node.js", "main": "dist/cli.js", "scripts": { - "build": "tsc", + "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 index 12b755d0..9efdbc5b 100644 --- a/e2e-cli/src/cli.ts +++ b/e2e-cli/src/cli.ts @@ -1,148 +1,19 @@ /** * E2E CLI for React Native analytics SDK testing * - * This CLI uses code copied directly from the SDK's api.ts and errors.ts - * to test the real HTTP upload and error handling behavior. + * 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 { v4 as uuidv4 } from "uuid"; - -// ============================================================================ -// Types (from SDK's types.ts) -// ============================================================================ - -interface SegmentEvent { - type: string; - anonymousId: string; - timestamp: string; - messageId: string; - context?: Record; - userId?: string; - event?: string; - properties?: Record; - traits?: Record; -} - -// ============================================================================ -// Copied from SDK's util.ts - getURL and validateURL -// ============================================================================ - -function getURL(host: string, path: string): string { - if (!host.startsWith("https://") && !host.startsWith("http://")) { - host = "https://" + host; - } - const s = `${host}${path}`; - if (!validateURL(s)) { - console.error("Invalid URL has been passed"); - console.log(`Invalid Url passed is ${s}`); - throw new Error("Invalid URL has been passed"); - } - return s; -} - -function validateURL(url: string): boolean { - const urlRegex = new RegExp( - "^(?:https?:\\/\\/)" + // Protocol (http or https) - "(?:\\S+(?::\\S*)?@)?" + // Optional user:pass@ - "(?:(localhost|\\d{1,3}(?:\\.\\d{1,3}){3})|" + // Localhost or IP address - "(?:(?!-)[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*(?:\\.[a-zA-Z]{2,})))" + // Domain validation (supports hyphens) - "(?::\\d{2,5})?" + // Optional port - "(\\/[^\\s?#]*)?" + // Path (allows `/projects/yup/settings`) - "(\\?[a-zA-Z0-9_.-]+=[a-zA-Z0-9_.-]+(&[a-zA-Z0-9_.-]+=[a-zA-Z0-9_.-]+)*)?" + // Query params - "(#[^\\s]*)?$", // Fragment (optional) - "i" // Case-insensitive - ); - return urlRegex.test(url); -} - -// ============================================================================ -// Copied from SDK's api.ts - uploadEvents -// ============================================================================ - -const uploadEvents = async ({ - writeKey, - url, - events, -}: { - writeKey: string; - url: string; - events: SegmentEvent[]; -}) => { - return await fetch(url, { - method: "POST", - body: JSON.stringify({ - batch: events, - sentAt: new Date().toISOString(), - writeKey: writeKey, - }), - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }); -}; - -// ============================================================================ -// Copied from SDK's errors.ts - checkResponseForErrors -// ============================================================================ - -enum ErrorType { - NetworkUnexpectedHTTPCode, - NetworkServerLimited, - NetworkServerRejected, - NetworkUnknown, -} - -class SegmentError extends Error { - type: ErrorType; - message: string; - innerError?: unknown; - - constructor(type: ErrorType, message: string, innerError?: unknown) { - super(message); - Object.setPrototypeOf(this, SegmentError.prototype); - this.type = type; - this.message = message; - this.innerError = innerError; - } -} - -class NetworkError extends SegmentError { - statusCode: number; - type: - | ErrorType.NetworkServerLimited - | ErrorType.NetworkServerRejected - | ErrorType.NetworkUnexpectedHTTPCode - | ErrorType.NetworkUnknown; - - constructor(statusCode: number, message: string, innerError?: unknown) { - let type: ErrorType; - if (statusCode === 429) { - type = ErrorType.NetworkServerLimited; - } else if (statusCode > 300 && statusCode < 400) { - type = ErrorType.NetworkUnexpectedHTTPCode; - } else if (statusCode >= 400) { - type = ErrorType.NetworkServerRejected; - } else { - type = ErrorType.NetworkUnknown; - } - - super(type, message, innerError); - Object.setPrototypeOf(this, NetworkError.prototype); - - this.statusCode = statusCode; - this.type = type as NetworkError["type"]; - } -} - -const checkResponseForErrors = (response: Response) => { - if (!response.ok) { - throw new NetworkError(response.status, response.statusText); - } - return response; -}; +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 @@ -174,43 +45,35 @@ interface CLIOutput { } // ============================================================================ -// Main CLI Logic +// In-memory Persistor for Node.js (replaces AsyncStorage) // ============================================================================ -function createEvent( - input: CLIInput["sequences"][0]["events"][0], - anonymousId: string -): SegmentEvent { - return { - type: input.type, - anonymousId, - timestamp: new Date().toISOString(), - messageId: uuidv4(), - context: { - library: { name: "analytics-react-native", version: "e2e-cli" }, - }, - ...(input.userId && { userId: input.userId }), - ...(input.event && { event: input.event }), - ...(input.properties && { properties: input.properties }), - ...(input.traits && { traits: input.traits }), - }; -} +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() { - // Parse command line arguments 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) { + if (args[i] === '--input' && i + 1 < args.length) { inputStr = args[i + 1]; break; } } if (!inputStr) { - console.error('Usage: cli --input \'{"writeKey":"...", ...}\''); - console.log(JSON.stringify({ success: false, error: "No input provided" })); + console.log(JSON.stringify({ success: false, error: 'No input provided' })); process.exit(1); } @@ -218,33 +81,94 @@ async function main() { try { const input: CLIInput = JSON.parse(inputStr); - const anonymousId = uuidv4(); - - // Build URL - uses SDK's getURL function (default matches SDK's internal default) - const url = getURL(input.apiHost ?? "api.segment.io", "/b"); - - // Collect events from all sequences - const allEvents: SegmentEvent[] = []; + // 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 eventInput of sequence.events) { - allEvents.push(createEvent(eventInput, anonymousId)); + 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; + } } } - // Upload batch - uses SDK's uploadEvents and checkResponseForErrors - if (allEvents.length > 0) { - const response = await uploadEvents({ - writeKey: input.writeKey, - url, - events: allEvents, - }); - checkResponseForErrors(response); - } + // 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) { 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 index e180e79f..c0c3118d 100644 --- a/e2e-cli/tsconfig.json +++ b/e2e-cli/tsconfig.json @@ -3,13 +3,18 @@ "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], - "outDir": "./dist", - "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "declaration": 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"]