Skip to content

Commit 6b2c70a

Browse files
committed
fix: bump windows for analytics tests
1 parent 95e13e3 commit 6b2c70a

4 files changed

Lines changed: 63 additions & 24 deletions

File tree

apps/e2e/tests/backend/endpoints/api/v1/analytics-events-batch.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,17 @@ it("accepts batch and debits event quota correctly", async ({ expect }) => {
528528
const { ownerTeamId } = await setupProjectWithPlan("free");
529529
await Auth.Otp.signIn();
530530

531-
// Drain async logEvent debits (sign-in triggers token-refresh/sign-up-rule events asynchronously) before measuring baseline.
532-
const quantityBeforeBatch = await waitForItemQuantityToStabilize(ownerTeamId, ITEM_IDS.analyticsEvents);
531+
// Drain async logEvent debits (sign-in triggers token-refresh/sign-up-rule
532+
// events asynchronously) before measuring baseline. The
533+
// `minimumElapsedMs` guards against the failure mode where stability is
534+
// declared before the async events have had a chance to fire — without
535+
// it the test reads e.g. 100000, declares it stable, then ~5s later the
536+
// async events land and the post-batch read is short by 2.
537+
const quantityBeforeBatch = await waitForItemQuantityToStabilize(
538+
ownerTeamId,
539+
ITEM_IDS.analyticsEvents,
540+
{ minimumElapsedMs: 5000 },
541+
);
533542

534543
const now = Date.now();
535544
const eventCount = 3;
@@ -561,7 +570,13 @@ it("rejects batch when remaining quota is less than batch size and does not debi
561570
// Drain async logEvent debits before forcing the quota down to a known
562571
// value — otherwise a trailing in-flight debit would push it negative
563572
// after we set it to 2 and break the post-condition.
564-
await waitForItemQuantityToStabilize(ownerTeamId, ITEM_IDS.analyticsEvents);
573+
// `minimumElapsedMs` guards against returning before the async events
574+
// have started firing.
575+
await waitForItemQuantityToStabilize(
576+
ownerTeamId,
577+
ITEM_IDS.analyticsEvents,
578+
{ minimumElapsedMs: 5000 },
579+
);
565580
await setItemQuantity(ownerTeamId, ITEM_IDS.analyticsEvents, 2);
566581

567582
const res = await uploadEventBatch({

apps/e2e/tests/backend/endpoints/api/v1/analytics-events.test.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,25 @@ const queryEventDataJson = async (params: {
6060
},
6161
});
6262

63+
// Defaults give 40 attempts * 500ms = ~20s of polling.
64+
//
65+
// The events under test are produced *asynchronously* by the sign-in path:
66+
// `runAsynchronouslyAndWaitUntil(logEvent)` fires after the HTTP response
67+
// returns and runs through SDK self-call → quota debit → Postgres insert →
68+
// ClickHouse async_insert (which is server-buffered, no wait_for_async_insert).
69+
// Under CI load this whole pipeline can take well over 10s before the row
70+
// becomes queryable, so the previous 7.5s window was still flaking with
71+
// "expected 0 to be greater than 0". 20s is conservative; the loop breaks
72+
// out as soon as the row appears, so there's no cost on the happy path.
73+
const DEFAULT_QUERY_RETRY_ATTEMPTS = 40;
74+
const DEFAULT_QUERY_RETRY_DELAY_MS = 500;
75+
6376
const fetchEventDataJsonWithRetry = async (
6477
params: { userId?: string, eventType?: string },
6578
options: { attempts?: number, delayMs?: number } = {}
6679
) => {
67-
const attempts = options.attempts ?? 5;
68-
const delayMs = options.delayMs ?? 250;
80+
const attempts = options.attempts ?? DEFAULT_QUERY_RETRY_ATTEMPTS;
81+
const delayMs = options.delayMs ?? DEFAULT_QUERY_RETRY_DELAY_MS;
6982

7083
let response = await queryEventDataJson(params);
7184
for (let attempt = 0; attempt < attempts; attempt++) {
@@ -87,8 +100,8 @@ const fetchEventsWithRetry = async (
87100
params: { userId?: string, eventType?: string },
88101
options: { attempts?: number, delayMs?: number } = {}
89102
) => {
90-
const attempts = options.attempts ?? 5;
91-
const delayMs = options.delayMs ?? 250;
103+
const attempts = options.attempts ?? DEFAULT_QUERY_RETRY_ATTEMPTS;
104+
const delayMs = options.delayMs ?? DEFAULT_QUERY_RETRY_DELAY_MS;
92105

93106
let response = await queryEvents(params);
94107
for (let attempt = 0; attempt < attempts; attempt++) {

apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/current/refresh-race.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ it("does not 500 when a refresh races with a sign-out of the same session", { ti
4545
mailbox: createMailbox(`refresh-race--${randomUUID()}${generatedEmailSuffix}`),
4646
userAuth: null,
4747
});
48-
await Auth.Password.signUpWithEmail();
48+
// `noWaitForEmail`: the race test only needs a refresh token, which is
49+
// returned by the sign-up response itself. Waiting for the verification
50+
// email costs 5–10s per iteration on CI and pushes the test past its
51+
// 120s timeout.
52+
await Auth.Password.signUpWithEmail({ noWaitForEmail: true });
4953
const rt = backendContext.value.userAuth!.refreshToken!;
5054

5155
const refreshP = niceBackendFetch("/api/v1/auth/sessions/current/refresh", {
@@ -84,7 +88,8 @@ it("does not 500 when an OAuth refresh-token grant races with a sign-out of the
8488
mailbox: createMailbox(`oauth-refresh-race--${randomUUID()}${generatedEmailSuffix}`),
8589
userAuth: null,
8690
});
87-
await Auth.Password.signUpWithEmail();
91+
// See note above on `noWaitForEmail`.
92+
await Auth.Password.signUpWithEmail({ noWaitForEmail: true });
8893
const rt = backendContext.value.userAuth!.refreshToken!;
8994
const projectKeys = backendContext.value.projectKeys;
9095
if (projectKeys === "no-project") throw new Error("No project keys found in the backend context");

apps/e2e/tests/backend/payment-quota-helpers.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,37 +76,45 @@ export async function waitForItemQuantityToReach(
7676
}
7777

7878
/**
79-
* Polls the item quantity every 500ms until it stops changing for 8 reads
80-
* in a row (~4 seconds of no movement), then returns the stable value.
81-
* Throws if no stable value is observed within 15 seconds.
79+
* Polls the item quantity every 500ms until it stops changing for
80+
* `stableForReads` reads in a row, then returns the stable value. Throws
81+
* if no stable value is observed within `timeoutMs`.
8282
*
8383
* Use this when you DON'T know the exact target — for example, after
8484
* `Auth.Otp.signIn()` triggers an unknown number of async logEvent debits
8585
* (token-refresh + sign-up-rule events) and you just want them to drain
8686
* before measuring a baseline.
8787
*
88-
* The 4-second stability window is deliberately conservative: a shorter
89-
* one (we tried 1.2s) exits during brief lulls between batches of late-
90-
* arriving sign-up-rule debits and lets ~2 extra debits land between the
91-
* baseline read and the next test request, breaking exact-quota
92-
* assertions.
88+
*
89+
* `options.minimumElapsedMs` (default 0) refuses to return until at least
90+
* that much wall time has passed since the function was called, even if
91+
* the quantity has been stable the whole time. This is useful when the
92+
* caller knows async events should fire but hasn't seen them yet — it
93+
* prevents the function from declaring stability before the async work
94+
* has even started.
9395
*/
9496
export async function waitForItemQuantityToStabilize(
9597
ownerTeamId: string,
9698
itemId: ItemId,
99+
options: { minimumElapsedMs?: number } = {},
97100
): Promise<number> {
98101
const pollIntervalMs = 500;
99-
const stableForReads = 8;
100-
const timeoutMs = 15000;
102+
const stableForReads = 16;
103+
const timeoutMs = 30000;
104+
const minimumElapsedMs = options.minimumElapsedMs ?? 0;
101105
const startedAt = performance.now();
102106

103107
let last = await getItemQuantity(ownerTeamId, itemId);
104108
let stableReads = 1;
105109

106-
while (stableReads < stableForReads) {
107-
if (performance.now() - startedAt > timeoutMs) {
110+
while (true) {
111+
const elapsed = performance.now() - startedAt;
112+
if (stableReads >= stableForReads && elapsed >= minimumElapsedMs) {
113+
return last;
114+
}
115+
if (elapsed > timeoutMs) {
108116
throw new StackAssertionError(`Item quantity did not stabilise within timeout`, {
109-
ownerTeamId, itemId, last, stableReads, stableForReads, timeoutMs,
117+
ownerTeamId, itemId, last, stableReads, stableForReads, timeoutMs, minimumElapsedMs,
110118
});
111119
}
112120

@@ -120,6 +128,4 @@ export async function waitForItemQuantityToStabilize(
120128
last = next;
121129
}
122130
}
123-
124-
return last;
125131
}

0 commit comments

Comments
 (0)