Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
972b5f0
feat: implement TAPI backoff and rate limiting
abueide Feb 9, 2026
f8f6df0
test: add comprehensive E2E and manual testing infrastructure for TAP…
abueide Feb 9, 2026
07418ac
test: add backoff E2E tests to main E2E suite
abueide Feb 9, 2026
46c1e72
fix: use functional dispatch pattern for Sovran stores
abueide Feb 9, 2026
0d75af2
fix: improve UUID mocking for BatchUploadManager tests
abueide Feb 9, 2026
b70262e
test: skip persistence tests that require real Sovran+AsyncStorage
abueide Feb 9, 2026
b71ae18
test: fix all failing unit tests and add persistence E2E test
abueide Feb 9, 2026
144dea6
fix: use consistent mock method in E2E tests
abueide Feb 9, 2026
2311b7c
fix: add null checks for batch ID when backoff managers undefined
abueide Feb 9, 2026
13ef9c7
fix: add storePersistor to E2E app for backoff state management
abueide Feb 10, 2026
2cf87d0
chore: standardize test task names across devbox and npm
abueide Feb 10, 2026
cc8f2fb
fix: make devbox run test run all test suites (unit + E2E)
abueide Feb 10, 2026
9a07db3
feat: add format check to test-fast command
abueide Feb 10, 2026
f917e95
fix: critical bug where permanent errors never dequeue from event queue
abueide Feb 11, 2026
19061dd
debug: add comprehensive logging to trace event flow
abueide Feb 11, 2026
d5bf16c
fix(e2e): remove CountFlushPolicy and adjust test timing
abueide Feb 11, 2026
e5c6943
test: remove backoff E2E tests (requires infrastructure improvements)
abueide Feb 11, 2026
e44711d
fix(e2e): fix race condition and add error handling for backoff initi…
abueide Feb 11, 2026
62cdd45
fix: remove early settingsResolve() call in configure() to fix race c…
abueide Feb 11, 2026
866b24f
fix: enable Logger by default regardless of NODE_ENV
abueide Feb 11, 2026
d957f83
fix(e2e): enable Logger plugin in release builds and restore backoff …
abueide Feb 11, 2026
c5262a5
fix(e2e): add storePersistor and disable CountFlushPolicy to fix rate…
abueide Feb 11, 2026
fc789cd
fix(e2e): enable Detox log recording to capture console.log output
abueide Feb 12, 2026
6b0d7dd
feat(e2e): add debug build scripts for console.log debugging
abueide Feb 12, 2026
90fe8c3
refactor(e2e): use E2E_DEBUG=1 env var for consistent test configuration
abueide Feb 12, 2026
06fe268
fix(e2e): start Metro bundler before running iOS E2E tests
abueide Feb 12, 2026
c6bd505
docs(e2e): document Metro bundler requirement for E2E tests
abueide Feb 12, 2026
dc1f024
docs(e2e): comprehensive E2E test logging analysis and findings
abueide Feb 12, 2026
8bd86d6
feat(e2e): implement proper Detox logging practices
abueide Feb 12, 2026
2c77a79
refactor: remove temporary console.log debugging code and use proper …
abueide Feb 12, 2026
49a4ecd
some changes
abueide Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,4 @@ packages/core/src/info.ts
.pnpm/

AGENTS.md

**/artifacts/**
229 changes: 229 additions & 0 deletions E2E_DIAGNOSIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# E2E Test Failures - Full Diagnostic Analysis

## Evidence Collected

### 1. Test Failure Pattern

From multiple test runs, consistent pattern observed:

- **Mock server receives**: Settings requests (`/v1/projects/yup/settings`) ✅
- **Mock server NEVER receives**: Batch upload requests (`/v1/b`) ❌
- **Error pattern**: `Expected number of calls: >= 1, Received number of calls: 0`

### 2. Test Behavior

```
Before each test:
1. Mock server starts successfully ("🚀 Started mock server on port 9091")
2. Settings endpoint works ("➡️ Replying with Settings")
3. Device reloads React Native
4. clearLifecycleEvents() is called (manual flush)
5. Test tracks events and calls flush
6. Mock server receives 0 batch upload requests
```

### 3. Code Changes from Master

**SegmentDestination.ts sendEvents() method:**

- Master: Used `Promise.all()` for parallel batch uploads
- Current: Changed to sequential `for` loop with `await uploadBatch()`
- Added: Backoff components (UploadStateMachine, BatchUploadManager)
- Added: `eventsToDequeue` tracking for permanent errors

**Key difference in dequeuing:**

```typescript
// MASTER: Dequeue in finally block after each batch
try {
await uploadEvents(...)
sentEvents = sentEvents.concat(batch);
} catch (e) {
...
} finally {
await this.queuePlugin.dequeue(sentEvents);
}

// CURRENT: Dequeue once at end after all batches
let eventsToDequeue: SegmentEvent[] = [];
for (const batch of chunkedEvents) {
const result = await this.uploadBatch(batch);
if (result.success || result.dropped) {
eventsToDequeue = eventsToDequeue.concat(batch);
}
}
await this.queuePlugin.dequeue(eventsToDequeue);
```

### 4. Backoff Component Initialization

In `SegmentDestination.update()` (line 259):

```typescript
void import('../backoff').then(({ UploadStateMachine, BatchUploadManager }) => {
this.uploadStateMachine = new UploadStateMachine(...);
this.batchUploadManager = new BatchUploadManager(...);
});
this.settingsResolve(); // Called immediately, not awaited
```

**Timing issue**: Settings are marked as loaded BEFORE backoff components finish initializing.

### 5. Upload Gate Check

In `sendEvents()` (line 50-56):

```typescript
if (this.uploadStateMachine) {
const canUpload = await this.uploadStateMachine.canUpload();
if (!canUpload) {
return Promise.resolve(); // Silently returns without uploading
}
}
```

**Issue**: If `uploadStateMachine` exists but isn't ready, it might return false and block uploads.

### 6. Mock Server Settings Response

```javascript
app.get('/v1/projects/yup/settings', (req, res) => {
res.status(200).send({
integrations: {
'Segment.io': {},
},
});
});
```

**Missing**: No `httpConfig` in settings response.
**Result**: Code uses `defaultHttpConfig` which has rate limiting ENABLED:

```typescript
const defaultHttpConfig: HttpConfig = {
rateLimitConfig: {
enabled: true, // ← Rate limiting ON by default
maxRetryCount: 100,
...
},
backoffConfig: {
enabled: true, // ← Backoff ON by default
...
},
};
```

### 7. Unit Test Success

All 423 unit tests pass, including backoff tests. **Why?**

- Unit tests mock the store and components directly
- Unit tests don't rely on dynamic import timing
- Unit tests test components in isolation

## Root Cause Hypothesis

### Most Likely: Race Condition in Component Initialization

**Sequence of events:**

1. App launches, settings loaded
2. `SegmentDestination.update()` called
3. `settingsResolve()` marks settings as ready (line 279)
4. `void import('../backoff')` starts async import (line 259)
5. First flush happens (lifecycle events) - backoff components likely NOT ready yet
- `this.uploadStateMachine` is undefined
- Upload proceeds successfully ✅
6. Backoff components finish initializing
7. Second flush happens (test events)
- `this.uploadStateMachine` now exists
- BUT the sovran store might not be initialized yet
- `canUpload()` might fail or return unexpected value
- Upload silently blocked ❌

### Secondary Issue: Store Initialization Timing

`UploadStateMachine` constructor creates a sovran store:

```typescript
this.store = createStore<UploadStateData>(
INITIAL_STATE, // state: 'READY'
persistor ? { persist: { storeId, persistor } } : undefined
);
```

**In E2E environment:**

- No persistor (App.tsx has it commented out)
- Store should be in-memory and immediate
- But `canUpload()` does `await this.store.getState()`
- First call might hit uninitialized store?

## Evidence Supporting This Hypothesis

1. ✅ First flush (lifecycle) works - backoff not ready yet
2. ✅ Subsequent flushes fail - backoff initialized but problematic
3. ✅ Unit tests pass - mocked components, no timing issues
4. ✅ Settings requests work - they happen before backoff init
5. ✅ No error messages - upload silently returns early

## Recommended Fix

### Option A: Await backoff initialization

```typescript
this.settingsPromise = (async () => {
await import('../backoff').then(({ UploadStateMachine, BatchUploadManager }) => {
this.uploadStateMachine = new UploadStateMachine(...);
this.batchUploadManager = new BatchUploadManager(...);
});
this.settingsResolve();
})();
```

### Option B: Make backoff components optional

```typescript
// Only check if components are FULLY initialized
if (this.uploadStateMachine && this.batchUploadManager) {
const canUpload = await this.uploadStateMachine.canUpload();
if (!canUpload) {
return Promise.resolve();
}
}
```

### Option C: Add ready state check

```typescript
private backoffReady = false;

void import('../backoff').then(({ UploadStateMachine, BatchUploadManager }) => {
this.uploadStateMachine = new UploadStateMachine(...);
this.batchUploadManager = new BatchUploadManager(...);
this.backoffReady = true;
});

// In sendEvents():
if (this.backoffReady && this.uploadStateMachine) {
const canUpload = await this.uploadStateMachine.canUpload();
...
}
```

## Additional Issues Found

### 1. Missing setMockBehavior in mockServer.js

The test files reference `setMockBehavior()` but it's not exported from mockServer.js on master branch.

### 2. Sequential vs Parallel Upload

Master used `Promise.all()` for parallel uploads. We changed to sequential. While this matches the SDD spec, it might interact poorly with the test timing.

## Next Steps for Validation

1. Add console.log to UploadStateMachine.canUpload() to see if it's being called
2. Add console.log to track backoff component initialization timing
3. Check if store.getState() is returning properly
4. Verify settingsPromise resolution timing relative to backoff init
10 changes: 7 additions & 3 deletions devbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@
"build": ["bash $SCRIPTS_DIR/build.sh"],
"format": ["treefmt"],
"lint": ["treefmt --fail-on-change"],
"test-android": ["bash $SCRIPTS_DIR/android/test.sh"],
"test-ios": ["bash $SCRIPTS_DIR/ios/test.sh"],
"act-ci": [
"bash $SCRIPTS_DIR/act-ci.sh --platform ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-24.04"
],
Expand Down Expand Up @@ -101,7 +99,13 @@
"echo \"iOS simulators shutdown (if any were running).\""
],
"stop": ["devbox run stop-android", "devbox run stop-ios"],
"test": ["devbox run test-android", "devbox run test-ios"]
"test-unit": ["yarn test:unit"],
"test-fast": ["yarn test:fast"],
"test-e2e-android": ["bash $SCRIPTS_DIR/android/test.sh"],
"test-e2e-ios": ["bash $SCRIPTS_DIR/ios/test.sh"],
"test-e2e": ["devbox run test-e2e-android", "devbox run test-e2e-ios"],
"test-all": ["devbox run test-fast", "devbox run test-e2e"],
"test": ["devbox run test-all"]
}
}
}
2 changes: 1 addition & 1 deletion examples/E2E-73/.detoxrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ module.exports = {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14',
type: 'iPhone 17',
},
},
attached: {
Expand Down
Loading
Loading