diff --git a/.github/workflows/setup-tests-with-custom-base-port.yaml b/.github/workflows/setup-tests-with-custom-base-port.yaml index b6f511ece..e8acdda9a 100644 --- a/.github/workflows/setup-tests-with-custom-base-port.yaml +++ b/.github/workflows/setup-tests-with-custom-base-port.yaml @@ -26,10 +26,10 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Setup Node.js v20 + - name: Setup Node.js v22 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: - node-version: 20 + node-version: 22 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 diff --git a/.github/workflows/setup-tests.yaml b/.github/workflows/setup-tests.yaml index 89ee109cd..1bdc1ebdd 100644 --- a/.github/workflows/setup-tests.yaml +++ b/.github/workflows/setup-tests.yaml @@ -24,10 +24,10 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Setup Node.js v20 + - name: Setup Node.js v22 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: - node-version: 20 + node-version: 22 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 diff --git a/apps/backend/src/lib/seed-dummy-data.ts b/apps/backend/src/lib/seed-dummy-data.ts index f217c15b2..11ce22585 100644 --- a/apps/backend/src/lib/seed-dummy-data.ts +++ b/apps/backend/src/lib/seed-dummy-data.ts @@ -681,7 +681,7 @@ async function seedDummyUsers(options: SeedDummyUsersOptions): Promise 0) { await tx.projectUserDirectPermission.createMany({ data: directPermissionRows }); } - }); + }, { timeout: 90_000 }); } // Team memberships for the named seed users — bulk-inserted the same way. @@ -725,7 +725,7 @@ async function seedDummyUsers(options: SeedDummyUsersOptions): Promise 0) { await tx.teamMemberDirectPermission.createMany({ data: teamMemberPermissionRows }); } - }); + }, { timeout: 90_000 }); } return userEmailToId; diff --git a/apps/backend/src/prisma-client.tsx b/apps/backend/src/prisma-client.tsx index 53c18cd08..94cba5609 100644 --- a/apps/backend/src/prisma-client.tsx +++ b/apps/backend/src/prisma-client.tsx @@ -480,7 +480,7 @@ class TransactionErrorThatShouldNotBeRetried extends Error { /** * @deprecated Prisma transactions are slow and lock the database. Use rawQuery with CTEs instead. Ask Konsti if you're confused or think you need transactions. */ -export async function retryTransaction(client: Omit, fn: (tx: PrismaClientTransaction) => Promise, options: { level?: "default" | "serializable" } = {}): Promise { +export async function retryTransaction(client: Omit, fn: (tx: PrismaClientTransaction) => Promise, options: { level?: "default" | "serializable", timeout?: number } = {}): Promise { // serializable transactions are currently off by default, later we may turn them on const enableSerializable = options.level === "serializable"; @@ -524,6 +524,7 @@ export async function retryTransaction(client: Omit, fn: return res; }, { isolationLevel: enableSerializable ? Prisma.TransactionIsolationLevel.Serializable : undefined, + ...(options.timeout != null ? { timeout: options.timeout } : {}), })); } catch (e) { // we don't want to retry too aggressively here, because the error may have been thrown after the transaction was already committed diff --git a/apps/e2e/tests/backend/endpoints/api/v1/analytics-events-batch.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/analytics-events-batch.test.ts index 0989df216..d63ef7f61 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/analytics-events-batch.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/analytics-events-batch.test.ts @@ -634,7 +634,7 @@ it("rejects batch when analytics event quota is exhausted", async ({ expect }) = expect(res.body.code).toBe("ITEM_QUANTITY_INSUFFICIENT_AMOUNT"); }); -it("accepts batch and debits event quota correctly", async ({ expect }) => { +it("accepts batch and debits event quota correctly", { timeout: 120_000 }, async ({ expect }) => { const { ownerTeamId } = await setupProjectWithPlan("free"); await Auth.Otp.signIn(); @@ -673,7 +673,7 @@ it("accepts batch and debits event quota correctly", async ({ expect }) => { // We don't support metered pricing or partial batches for now, so the entire // batch is rejected when remaining quota is less than the batch size, and // the quota must remain unchanged (no partial debit). -it("rejects batch when remaining quota is less than batch size and does not debit", async ({ expect }) => { +it("rejects batch when remaining quota is less than batch size and does not debit", { timeout: 120_000 }, async ({ expect }) => { const { ownerTeamId } = await setupProjectWithPlan("free"); await Auth.Otp.signIn(); diff --git a/apps/e2e/tests/backend/endpoints/api/v1/analytics-events.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/analytics-events.test.ts index a7dd30283..526e18225 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/analytics-events.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/analytics-events.test.ts @@ -214,7 +214,7 @@ it("cannot read events from other projects", async ({ expect }) => { `); }); -it("filters analytics events by user within a project", async ({ expect }) => { +it("filters analytics events by user within a project", { timeout: 120_000 }, async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true } }); const { userId: userA } = await Auth.Otp.signIn(); await bumpEmailAddress(); diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts index cc33e34bb..958e99a3a 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts @@ -43,7 +43,7 @@ async function ensureAnonymousUsersAreStillExcluded(metricsResponse: NiceRespons // ClickHouse ingestion is async; poll until anonymous users are excluded again. let response!: NiceResponse; - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 30; i++) { await wait(2_000); response = await niceBackendFetch("/api/v1/internal/metrics", { accessType: 'admin' }); const noAnonymousInRecentlyRegistered = (response.body.recently_registered as MetricsUser[]).every((user) => !user.is_anonymous); @@ -72,7 +72,7 @@ async function ensureAnonymousUsersAreStillExcluded(metricsResponse: NiceRespons async function waitForMetricsToIncludeUsersByCountry(options: { countryCode: string, expectedCount: number }): Promise { let response!: NiceResponse; - for (let i = 0; i < 15; i++) { + for (let i = 0; i < 30; i++) { response = await niceBackendFetch("/api/v1/internal/metrics", { accessType: 'admin' }); if (response.body?.users_by_country?.[options.countryCode] === options.expectedCount) { return response; @@ -88,7 +88,7 @@ async function waitForMetricsMatch( ): Promise { let response!: NiceResponse; const suffix = includeAnonymous ? "?include_anonymous=true" : ""; - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 60; i++) { response = await niceBackendFetch(`/api/v1/internal/metrics${suffix}`, { accessType: 'admin' }); if (predicate(response)) { return response; @@ -123,7 +123,7 @@ async function waitForAnalyticsRowsForSessionReplaySegment( throw new Error(`Timed out waiting for ${expectedCount} analytics rows for session replay segment ${sessionReplaySegmentId}`); } -it("should return metrics data", async ({ expect }) => { +it("should return metrics data", { timeout: 120_000 }, async ({ expect }) => { await Project.createAndSwitch({ config: { magic_link_enabled: true, diff --git a/apps/e2e/tests/backend/endpoints/api/v1/token-refresh-events.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/token-refresh-events.test.ts index b3d726a3d..c333c3521 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/token-refresh-events.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/token-refresh-events.test.ts @@ -189,7 +189,7 @@ it("anonymous signup creates exactly one $token-refresh event", async ({ expect }); }); -it("OAuth signup creates exactly one $token-refresh event", async ({ expect }) => { +it("OAuth signup creates exactly one $token-refresh event", { timeout: 120_000 }, async ({ expect }) => { const { projectId } = await Project.createAndSwitch({ config: { oauth_providers: [{ @@ -223,7 +223,7 @@ it("OAuth signup creates exactly one $token-refresh event", async ({ expect }) = // Signin Tests // ============================================================================ -it("password signin (existing user) creates exactly one additional $token-refresh event", async ({ expect }) => { +it("password signin (existing user) creates exactly one additional $token-refresh event", { timeout: 120_000 }, async ({ expect }) => { const { projectId } = await Project.createAndSwitch({ config: { credential_enabled: true }, }); @@ -246,7 +246,7 @@ it("password signin (existing user) creates exactly one additional $token-refres expect(events.every((e: AnalyticsEvent) => e.user_id === userId)).toBe(true); }); -it("OTP signin (existing user) creates exactly one additional $token-refresh event", async ({ expect }) => { +it("OTP signin (existing user) creates exactly one additional $token-refresh event", { timeout: 120_000 }, async ({ expect }) => { const { projectId } = await Project.createAndSwitch({ config: { magic_link_enabled: true }, }); @@ -267,7 +267,7 @@ it("OTP signin (existing user) creates exactly one additional $token-refresh eve expect(events.every((e: AnalyticsEvent) => e.user_id === userId)).toBe(true); }); -it("OAuth signin (existing user) creates exactly one additional $token-refresh event", async ({ expect }) => { +it("OAuth signin (existing user) creates exactly one additional $token-refresh event", { timeout: 120_000 }, async ({ expect }) => { const { projectId } = await Project.createAndSwitch({ config: { oauth_providers: [{ @@ -299,7 +299,7 @@ it("OAuth signin (existing user) creates exactly one additional $token-refresh e // Session Refresh Tests // ============================================================================ -it("session refresh endpoint creates exactly one additional $token-refresh event", async ({ expect }) => { +it("session refresh endpoint creates exactly one additional $token-refresh event", { timeout: 120_000 }, async ({ expect }) => { const { projectId } = await Project.createAndSwitch({ config: { magic_link_enabled: true }, }); @@ -317,7 +317,7 @@ it("session refresh endpoint creates exactly one additional $token-refresh event expect(events.every((e: AnalyticsEvent) => e.user_id === userId)).toBe(true); }); -it("multiple session refreshes create one event each", async ({ expect }) => { +it("multiple session refreshes create one event each", { timeout: 180_000 }, async ({ expect }) => { const { projectId } = await Project.createAndSwitch({ config: { magic_link_enabled: true }, }); @@ -344,7 +344,7 @@ it("multiple session refreshes create one event each", async ({ expect }) => { // OAuth Refresh Token Grant Tests // ============================================================================ -it("OAuth refresh token grant creates exactly one additional $token-refresh event", async ({ expect }) => { +it("OAuth refresh token grant creates exactly one additional $token-refresh event", { timeout: 120_000 }, async ({ expect }) => { const { projectId } = await Project.createAndSwitch({ config: { oauth_providers: [{ @@ -391,7 +391,7 @@ it("OAuth refresh token grant creates exactly one additional $token-refresh even expect(events.every((e: AnalyticsEvent) => e.user_id === userId)).toBe(true); }); -it("multiple OAuth refresh token grants create one event each", async ({ expect }) => { +it("multiple OAuth refresh token grants create one event each", { timeout: 180_000 }, async ({ expect }) => { const { projectId } = await Project.createAndSwitch({ config: { oauth_providers: [{ diff --git a/apps/e2e/tests/js/team-invitations.test.ts b/apps/e2e/tests/js/team-invitations.test.ts index 83782943a..10b1b1e99 100644 --- a/apps/e2e/tests/js/team-invitations.test.ts +++ b/apps/e2e/tests/js/team-invitations.test.ts @@ -198,7 +198,7 @@ it("should list invitations from multiple teams", async ({ expect }) => { }); -it("should accept a team invitation via the client SDK", async ({ expect }) => { +it("should accept a team invitation via the client SDK", { timeout: 120_000 }, async ({ expect }) => { const { clientApp, serverApp } = await createApp({ config: { clientTeamCreationEnabled: true } }); // Create a team