diff --git a/apps/backend/src/lib/config.tsx b/apps/backend/src/lib/config.tsx
index f6585f95d5..9c535d095c 100644
--- a/apps/backend/src/lib/config.tsx
+++ b/apps/backend/src/lib/config.tsx
@@ -1091,7 +1091,12 @@ import.meta.vitest?.test('setEnvironmentConfigOverride blocks writes for develop
const developmentEnvironment = await import("./development-environment");
- const isDevelopmentEnvironmentProjectSpy = vi.spyOn(developmentEnvironment, "isDevelopmentEnvironmentProject").mockResolvedValue(true);
+ // Spy on getEnvironmentConfigWriteBlockReason directly, because spying on
+ // isDevelopmentEnvironmentProject does not intercept intra-module calls
+ // (the function is called directly within the same module, not through
+ // the module namespace export).
+ const spy = vi.spyOn(developmentEnvironment, "getEnvironmentConfigWriteBlockReason")
+ .mockResolvedValue(DEVELOPMENT_ENVIRONMENT_ENV_CONFIG_BLOCKED_MESSAGE);
try {
await expect(setEnvironmentConfigOverride({
@@ -1100,7 +1105,7 @@ import.meta.vitest?.test('setEnvironmentConfigOverride blocks writes for develop
environmentConfigOverride: {},
})).rejects.toThrow(DEVELOPMENT_ENVIRONMENT_ENV_CONFIG_BLOCKED_MESSAGE);
} finally {
- isDevelopmentEnvironmentProjectSpy.mockRestore();
+ spy.mockRestore();
}
});
diff --git a/apps/backend/src/lib/email-queue-step.tsx b/apps/backend/src/lib/email-queue-step.tsx
index c2864d322b..09e857b2f2 100644
--- a/apps/backend/src/lib/email-queue-step.tsx
+++ b/apps/backend/src/lib/email-queue-step.tsx
@@ -734,7 +734,6 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
return;
}
}
-
const result = getEnvBoolean("STACK_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING")
? Result.error({ errorType: "email-sending-disabled", canRetry: false, message: "Email sending is disabled", rawError: new Error("Email sending is disabled") })
: await lowLevelSendEmailDirectWithoutRetries({
@@ -745,7 +744,6 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
html: row.renderedHtml ?? undefined,
text: row.renderedText ?? undefined,
});
-
if (result.status === "error") {
const newAttemptCount = row.sendRetries + 1;
const isAttemptsExhausted = result.error.canRetry && newAttemptCount >= MAX_SEND_ATTEMPTS;
diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts
index c4b64b9f47..603a6454dd 100644
--- a/apps/backend/vitest.config.ts
+++ b/apps/backend/vitest.config.ts
@@ -7,7 +7,7 @@ export default mergeConfig(
sharedConfig,
defineConfig({
test: {
- testTimeout: 20000,
+ testTimeout: 60000,
env: {
...loadEnv('', process.cwd(), ''),
...loadEnv('development', process.cwd(), ''),
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx
index 39f388efd7..bbedefbd7d 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx
@@ -1630,9 +1630,9 @@ function Draggable(props: {
A runtime error occured while rendering this widget.
- {props.reset && }
+
{errorToNiceString(props.error)}
diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts
index d7c83aeecf..b0b1e02398 100644
--- a/apps/e2e/tests/backend/backend-helpers.ts
+++ b/apps/e2e/tests/backend/backend-helpers.ts
@@ -483,20 +483,22 @@ export namespace Auth {
"headers": Headers { },
}
`);
- for (let i = 0; true; i++) {
+ const deadlineMs = 60_000;
+ const intervalMs = 500;
+ const deadline = performance.now() + deadlineMs;
+ while (true) {
const messages = await mailbox.fetchMessages();
const containsSubstring = messages.some(message => message.subject.includes("Sign in to") && message.body?.html.includes(response.body.nonce));
if (containsSubstring) {
break;
}
- await wait(100 + i * 20);
- if (i >= 40) {
- throw new StackAssertionError(`Sign-in code message not found after ${i} attempts`, {
+ if (performance.now() >= deadline) {
+ throw new StackAssertionError(`Sign-in code message not found within ${deadlineMs}ms`, {
response,
messages: messages.map(m => ({ ...m, body: m.body && omit(m.body, ["html"]) })),
- outboxEmails: await getOutboxEmails(),
});
}
+ await wait(intervalMs);
}
return {
sendSignInCodeResponse: response,
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 e2e0f09d73..3dd1311678 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
@@ -171,11 +171,11 @@ it("stores $token-refresh data in snake_case without row identity fields", async
it("cannot read events from other projects", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
const projectAKeys = backendContext.value.projectKeys;
- await Auth.Otp.signIn();
+ await Auth.fastSignUp();
// Switch to another project and generate its own event
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
- const { userId: projectBUserId } = await Auth.Otp.signIn();
+ const { userId: projectBUserId } = await Auth.fastSignUp();
const projectBResponse = await fetchEventsWithRetry({
userId: projectBUserId,
eventType: "$token-refresh",
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/index.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/index.test.ts
index 5766ecef34..479269e127 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/index.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/auth/sessions/index.test.ts
@@ -263,12 +263,12 @@ it("cannot delete current session as client", async ({ expect }) => {
});
it("cannot read another user's sessions as client", async ({ expect }) => {
- // Create first user and sign up
- const user1 = await Auth.Password.signUpWithEmail();
+ // Create first user and sign up (skip email wait — not needed for this test)
+ const user1 = await Auth.Password.signUpWithEmail({ noWaitForEmail: true });
// Create second user and sign up
backendContext.set({ userAuth: null, mailbox: createMailbox() }); // Clear first user's auth
- const user2 = await Auth.Password.signUpWithEmail();
+ const user2 = await Auth.Password.signUpWithEmail({ noWaitForEmail: true });
// Try to read user1's sessions while authenticated as user2
const listResponse = await niceBackendFetch("/api/v1/auth/sessions", {
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/emails/delivery-info.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/emails/delivery-info.test.ts
index c0faa3cb6c..ee06700311 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/emails/delivery-info.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/emails/delivery-info.test.ts
@@ -129,7 +129,7 @@ describe("with valid credentials", () => {
});
it("should track sent emails", async ({ expect }) => {
- await Auth.Otp.signIn();
+ await Auth.fastSignUp();
await Project.createAndSwitch({
display_name: "Test Sent Stats Project",
config: {
@@ -167,50 +167,32 @@ describe("with valid credentials", () => {
}
`);
- // wait for the email to be processed
- await wait(12_000);
-
- const response = await niceBackendFetch("/api/v1/emails/delivery-info", {
- method: "GET",
- accessType: "server",
- });
-
- expect(response).toMatchInlineSnapshot(`
- NiceResponse {
- "status": 200,
- "body": {
- "capacity": {
- "boost_expires_at": null,
- "boost_multiplier": 1,
- "is_boost_active": false,
- "penalty_factor": 1,
- "rate_per_second": 27.777779320987655,
- },
- "stats": {
- "day": {
- "bounced": 0,
- "marked_as_spam": 0,
- "sent": 1,
- },
- "hour": {
- "bounced": 0,
- "marked_as_spam": 0,
- "sent": 1,
- },
- "month": {
- "bounced": 0,
- "marked_as_spam": 0,
- "sent": 1,
- },
- "week": {
- "bounced": 0,
- "marked_as_spam": 0,
- "sent": 1,
- },
- },
- },
- "headers": Headers { },
+ // Poll until email stats are updated instead of a fixed wait
+ let response;
+ for (let i = 0; i < 30; i++) {
+ await wait(1_000);
+ response = await niceBackendFetch("/api/v1/emails/delivery-info", {
+ method: "GET",
+ accessType: "server",
+ });
+ if (response.status === 200 && response.body?.stats?.hour?.sent >= 1) {
+ break;
}
- `);
+ }
+
+ expect(response!.status).toBe(200);
+ const { stats, capacity } = response!.body;
+ expect(stats.hour.sent).toBe(1);
+ expect(stats.day.sent).toBe(1);
+ expect(stats.week.sent).toBe(1);
+ expect(stats.month.sent).toBe(1);
+ expect(stats.hour.bounced).toBe(0);
+ expect(stats.hour.marked_as_spam).toBe(0);
+ expect(capacity.is_boost_active).toBe(false);
+ expect(capacity.boost_expires_at).toBe(null);
+ expect(capacity.boost_multiplier).toBe(1);
+ expect(capacity.penalty_factor).toBe(1);
+ expect(capacity.rate_per_second).toBeGreaterThan(27);
+ expect(capacity.rate_per_second).toBeLessThan(28);
});
});
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/provision.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/provision.test.ts
index 331c48599e..cd71b2d406 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/provision.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/provision.test.ts
@@ -70,6 +70,7 @@ it("should be able to provision a new project if client details are correct", as
"description": "Project created by an external integration",
"display_name": "Test project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts
index 261f2f0199..7f3d3be281 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts
@@ -127,6 +127,7 @@ it("lists oauth providers", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts
index d3e6a268a1..527586e014 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts
@@ -40,6 +40,7 @@ it("get project details", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -98,6 +99,7 @@ it("creates and updates the basic project information of a project", async ({ ex
"description": "Updated description",
"display_name": "Updated Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": true,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -172,6 +174,7 @@ it("creates and updates the email config of a project", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/provision.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/provision.test.ts
index e67e4a0d29..5dc5e132db 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/provision.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/provision.test.ts
@@ -71,6 +71,7 @@ it("should be able to provision a new project if neon client details are correct
"description": "Created with Neon",
"display_name": "Test project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
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 4ea0cb2257..cc33e34bb1 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
@@ -173,7 +173,12 @@ it("should return metrics data with users", async ({ expect }) => {
backendContext.set({ mailbox: mailboxes[2], ipData: { country: "CH", ipAddress: "127.0.0.1", city: "Zurich", region: "ZH", latitude: 47.3769, longitude: 8.5417, tzIdentifier: "Europe/Zurich" } });
await Auth.Otp.signIn();
- const response = await waitForMetricsToIncludeUsersByCountry({ countryCode: "CH", expectedCount: 1 });
+ const response = await waitForMetricsMatch(
+ false,
+ (r) =>
+ r.body?.users_by_country?.["CH"] === 1 &&
+ (r.body?.active_users_by_country?.["AQ"]?.length ?? 0) >= 2,
+ );
expect(response).toMatchSnapshot(`metrics_result_with_users`);
await ensureAnonymousUsersAreStillExcluded(response);
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts
index 1b52da14cb..f13c3435d3 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts
@@ -6,7 +6,7 @@ import { it } from "../../../../../helpers";
import { backendContext, niceBackendFetch } from "../../../../backend-helpers";
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === "true";
-const blockedMessage = "cannot be changed in the local emulator";
+const blockedMessage = "cannot be changed in a development environment";
const localEmulatorProjectEndpoint = "/api/v1/internal/local-emulator/project";
async function switchToLocalEmulatorProject() {
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts
index f29fa73ed6..b07a57e2c8 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts
@@ -106,6 +106,7 @@ it("creates a new project", async ({ expect }) => {
"description": "",
"display_name": "Test Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -163,6 +164,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "Test description",
"display_name": "Test Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": true,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -222,6 +224,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "",
"display_name": "Test Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -272,6 +275,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "",
"display_name": "Test Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -336,6 +340,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "",
"display_name": "Test Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -402,6 +407,7 @@ it("creates a new project with different configurations", async ({ expect }) =>
"description": "",
"display_name": "Test Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -451,6 +457,7 @@ it("lists the current projects after creating a new project", async ({ expect })
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -506,6 +513,7 @@ it("verifies email_theme update persists", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -550,6 +558,7 @@ it("verifies email_theme update persists", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -604,6 +613,7 @@ it("updates trusted domains without modifying allow_localhost", async ({ expect
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -661,6 +671,7 @@ it("updates trusted domains without modifying allow_localhost", async ({ expect
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -735,6 +746,7 @@ it("lets user update logo_url to a valid image", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts
index 878c633385..7618608d23 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/transactions-refund.test.ts
@@ -908,7 +908,7 @@ async function createLiveModeSubscriptionWithRenewal(): Promise<{
};
}
-it("refunds a renewal invoice (invoice_id path) without money or revoke — sourceTxnId is sub-renewal", async () => {
+it("refunds a renewal invoice (invoice_id path) without money or revoke — sourceTxnId is sub-renewal", { timeout: 120_000 }, async () => {
const { subscriptionId, renewalInvoiceId } = await createLiveModeSubscriptionWithRenewal();
// Use amount_usd=0 and end_action='at-period-end' to exercise the
@@ -964,7 +964,7 @@ it("refunds a renewal invoice (invoice_id path) without money or revoke — sour
expect(refundRow).toBeDefined();
});
-it("rejects end_action='now' when invoice_id targets a renewal invoice", async () => {
+it("rejects end_action='now' when invoice_id targets a renewal invoice", { timeout: 120_000 }, async () => {
// The product grant lives on the sub-start txn, not on renewals — so a
// revocation entry referencing a renewal would point at a non-existent
// entry. Force admin to end-immediately against the start invoice (or the
@@ -987,7 +987,7 @@ it("rejects end_action='now' when invoice_id targets a renewal invoice", async (
expect(refundRes.body.error).toMatch(/Cannot end product access immediately when refunding a renewal invoice/);
});
-it("rejects refund with invoice_id that does not belong to the subscription", async () => {
+it("rejects refund with invoice_id that does not belong to the subscription", { timeout: 120_000 }, async () => {
const { subscriptionId } = await createLiveModeSubscriptionWithRenewal();
const unrelatedInvoiceId = randomUUID();
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts
index 7aa811e62c..6282fb2c3d 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/payments/items.test.ts
@@ -383,12 +383,17 @@ it("allows team admins to be added when item quantity is increased", async ({ ex
return mailbox;
}));
+ // Pre-fetch all invitation emails in parallel to avoid sequential waits
+ const allInvitationMessages = await Promise.all(
+ mailboxes.map((mailbox) => mailbox.waitForMessagesWithSubject("join")),
+ );
+
for (let i = 0; i < mailboxes.length; i++) {
const mailbox = mailboxes[i];
backendContext.set({ mailbox: mailbox });
await Auth.fastSignUp();
- const invitationMessages = await mailbox.waitForMessagesWithSubject("join");
+ const invitationMessages = allInvitationMessages[i];
const acceptResponse = await niceBackendFetch("/api/v1/team-invitations/accept", {
method: "POST",
accessType: "client",
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts
index e2f9af65ea..5a041ffc92 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts
@@ -238,6 +238,7 @@ it("can customize default user permissions", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts
index c0616c9025..26107451a8 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts
@@ -99,6 +99,7 @@ it("creates and updates the basic project information of a project", async ({ ex
"description": "Updated description",
"display_name": "Updated Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": true,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -152,6 +153,7 @@ it("updates the basic project configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -210,6 +212,7 @@ it("updates the project domains configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -274,6 +277,7 @@ it("updates the project domains configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -332,6 +336,7 @@ it("should allow insecure HTTP connections if insecureHttp is true", async ({ ex
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -433,6 +438,7 @@ it("updates the project email configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -496,6 +502,7 @@ it("updates the project email configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -545,6 +552,7 @@ it("updates the project email configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -594,6 +602,7 @@ it("updates the project email configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -657,6 +666,7 @@ it("updates the project email configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -833,6 +843,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -889,6 +900,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -949,6 +961,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -1004,6 +1017,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -1074,6 +1088,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
@@ -1144,6 +1159,7 @@ it("updates the project oauth configuration", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts
index 02d2b3c78c..826712a544 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/session-replays.test.ts
@@ -810,7 +810,7 @@ it("admin events endpoint does not allow fetching a chunk via the wrong session
await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } });
// session1: upload under first refresh token
- await Auth.Otp.signIn();
+ await Auth.fastSignUp();
const batchId = randomUUID();
const upload1 = await uploadBatch({
browserSessionId: randomUUID(),
@@ -823,7 +823,7 @@ it("admin events endpoint does not allow fetching a chunk via the wrong session
const recording1 = upload1.body?.session_replay_id;
// session2: upload under a different refresh token
- await Auth.Otp.signIn();
+ await Auth.fastSignUp();
const upload2 = await uploadBatch({
browserSessionId: randomUUID(),
batchId: randomUUID(),
@@ -971,12 +971,28 @@ async function listReplays(queryParams: Record = {}) {
});
}
+async function listReplaysWithRetry(
+ queryParams: Record,
+ predicate: (res: Awaited>) => boolean,
+ options: { attempts?: number, delayMs?: number } = {},
+) {
+ const attempts = options.attempts ?? 30;
+ const delayMs = options.delayMs ?? 500;
+ let res = await listReplays(queryParams);
+ for (let i = 0; i < attempts; i++) {
+ if (predicate(res)) return res;
+ await wait(delayMs);
+ res = await listReplays(queryParams);
+ }
+ return res;
+}
+
it("admin list session replays filters by user_ids", async ({ expect }) => {
await Project.createAndSwitch({ config: { magic_link_enabled: true } });
await Project.updateConfig({ apps: { installed: { analytics: { enabled: true } } } });
// User A
- const userA = await Auth.Otp.signIn();
+ const userA = await Auth.fastSignUp();
const uploadA = await uploadBatch({
browserSessionId: randomUUID(),
batchId: randomUUID(),
@@ -987,8 +1003,7 @@ it("admin list session replays filters by user_ids", async ({ expect }) => {
expect(uploadA.status).toBe(200);
// User B
- await bumpEmailAddress();
- const userB = await Auth.Otp.signIn();
+ const userB = await Auth.fastSignUp();
const uploadB = await uploadBatch({
browserSessionId: randomUUID(),
batchId: randomUUID(),
@@ -998,6 +1013,14 @@ it("admin list session replays filters by user_ids", async ({ expect }) => {
});
expect(uploadB.status).toBe(200);
+ // Wait for ClickHouse to ingest both replays before asserting filters
+ const resBoth = await listReplaysWithRetry(
+ { user_ids: `${userA.userId},${userB.userId}` },
+ (res) => res.status === 200 && res.body?.items?.length === 2,
+ );
+ expect(resBoth.status).toBe(200);
+ expect(resBoth.body?.items?.length).toBe(2);
+
// Filter by user A only
const resA = await listReplays({ user_ids: userA.userId });
expect(resA.status).toBe(200);
@@ -1010,11 +1033,6 @@ it("admin list session replays filters by user_ids", async ({ expect }) => {
expect(resB.body?.items?.length).toBe(1);
expect(resB.body?.items?.[0]?.project_user?.id).toBe(userB.userId);
- // Filter by both users
- const resBoth = await listReplays({ user_ids: `${userA.userId},${userB.userId}` });
- expect(resBoth.status).toBe(200);
- expect(resBoth.body?.items?.length).toBe(2);
-
// Filter by nonexistent user
const resNone = await listReplays({ user_ids: randomUUID() });
expect(resNone.status).toBe(200);
@@ -1183,7 +1201,7 @@ it("admin list session replays filters by click_count_min", async ({ expect }) =
const now = Date.now();
// Replay A: user with 3 clicks
- await Auth.Otp.signIn();
+ await Auth.fastSignUp();
const segmentIdA = randomUUID();
const uploadA = await uploadBatch({
browserSessionId: randomUUID(),
@@ -1222,8 +1240,7 @@ it("admin list session replays filters by click_count_min", async ({ expect }) =
expect(eventBatchA.status).toBe(200);
// Replay B: user with 1 click
- await bumpEmailAddress();
- await Auth.Otp.signIn();
+ await Auth.fastSignUp();
const segmentIdB = randomUUID();
const uploadB = await uploadBatch({
browserSessionId: randomUUID(),
@@ -1246,21 +1263,20 @@ it("admin list session replays filters by click_count_min", async ({ expect }) =
});
expect(eventBatchB.status).toBe(200);
- // Retry loop for ClickHouse eventual consistency
- let foundOnlyA = false;
- for (let i = 0; i < 15; i++) {
- const res = await listReplays({ click_count_min: "2" });
- expect(res.status).toBe(200);
- if (res.body?.items?.length === 1 && res.body?.items?.[0]?.id === replayIdA) {
- foundOnlyA = true;
- break;
- }
- await wait(500);
- }
- expect(foundOnlyA).toBe(true);
+ // Wait for ClickHouse to ingest click events and replays
+ const resClickMin = await listReplaysWithRetry(
+ { click_count_min: "2" },
+ (res) => res.status === 200 && res.body?.items?.length === 1 && res.body?.items?.[0]?.id === replayIdA,
+ );
+ expect(resClickMin.status).toBe(200);
+ expect(resClickMin.body?.items?.length).toBe(1);
+ expect(resClickMin.body?.items?.[0]?.id).toBe(replayIdA);
// click_count_min=0 should return both (no-op filter)
- const resAll = await listReplays({ click_count_min: "0" });
+ const resAll = await listReplaysWithRetry(
+ { click_count_min: "0" },
+ (res) => res.status === 200 && (res.body?.items?.length ?? 0) >= 2,
+ );
expect(resAll.status).toBe(200);
expect(resAll.body?.items?.length).toBeGreaterThanOrEqual(2);
});
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts
index e5cde06b38..fa7de78c07 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts
@@ -256,6 +256,7 @@ it("can customize default team permissions", async ({ expect }) => {
"description": "",
"display_name": "New Project",
"id": "",
+ "is_development_environment": false,
"is_production_mode": false,
"logo_dark_mode_url": null,
"logo_full_dark_mode_url": null,
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 d0223cd243..173db0b66c 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
@@ -45,8 +45,8 @@ const fetchEventsWithRetry = async (
params: { userId?: string, eventType?: string },
options: { attempts?: number, delayMs?: number, expectedCount?: number } = {}
) => {
- const attempts = options.attempts ?? 10;
- const delayMs = options.delayMs ?? 300;
+ const attempts = options.attempts ?? 40;
+ const delayMs = options.delayMs ?? 500;
const expectedCount = options.expectedCount ?? 1;
let response = await queryEvents(params);
@@ -77,7 +77,7 @@ const expectExactlyNTokenRefreshEvents = async (
// First, wait for events to appear
const response = await fetchEventsWithRetry(
{ userId, eventType: "$token-refresh" },
- { expectedCount, attempts: 15, delayMs: 300 }
+ { expectedCount }
);
if (response.status !== 200) {
@@ -322,7 +322,7 @@ it("multiple session refreshes create one event each", async ({ expect }) => {
});
await InternalApiKey.createAndSetProjectKeys();
- const { userId } = await Auth.Otp.signIn();
+ const { userId } = await Auth.fastSignUp();
await expectExactlyNTokenRefreshEvents(userId, 1, { projectId });
// Refresh multiple times
diff --git a/apps/e2e/tests/backend/endpoints/api/v1/unsubscribe-link.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/unsubscribe-link.test.ts
index 698190684d..ccd6c502e9 100644
--- a/apps/e2e/tests/backend/endpoints/api/v1/unsubscribe-link.test.ts
+++ b/apps/e2e/tests/backend/endpoints/api/v1/unsubscribe-link.test.ts
@@ -93,6 +93,7 @@ it("unsubscribe link should not be sent for emails with transactional notificati
await Project.createAndSwitch({
display_name: "Test Successful Email Project",
config: {
+ credential_enabled: true,
email_config: {
type: "standard",
host: "localhost",
@@ -104,7 +105,7 @@ it("unsubscribe link should not be sent for emails with transactional notificati
},
},
});
- const { userId } = await Auth.Password.signUpWithEmail();
+ const { userId } = await Auth.Password.signUpWithEmail({ noWaitForEmail: true });
const response = await niceBackendFetch(
"/api/v1/emails/send-email",
{