diff --git a/apps/e2e/tests/backend/endpoints/api/v1/__snapshots__/internal-metrics.test.ts.snap b/apps/e2e/tests/backend/endpoints/api/v1/__snapshots__/internal-metrics.test.ts.snap index 3e6e392ad..8bd520ef5 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/__snapshots__/internal-metrics.test.ts.snap +++ b/apps/e2e/tests/backend/endpoints/api/v1/__snapshots__/internal-metrics.test.ts.snap @@ -8,6 +8,7 @@ NiceResponse { "analytics_overview": { "anonymous_visitors_fallback": 0, "avg_session_seconds": 0, + "bounce_rate": 0, "daily_anonymous_visitors_fallback": [ { "activity": 0, @@ -134,258 +135,10 @@ NiceResponse { "date": , }, ], - "daily_clicks": [ - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - ], - "daily_page_views": [ - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - ], + "daily_avg_session_seconds": [], + "daily_bounce_rate": [], + "daily_clicks": [], + "daily_page_views": [], "daily_revenue": [ { "date": , @@ -543,137 +296,19 @@ NiceResponse { "refund_cents": 0, }, ], - "daily_visitors": [ - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - ], + "daily_visitors": [], + "hourly_active_users": [], + "hourly_page_views": [], + "hourly_visitors": [], "online_live": 0, "recent_replays": 0, "revenue_per_visitor": 0, + "top_browsers": [], + "top_devices": [], + "top_operating_systems": [], "top_referrers": [], "top_region": null, + "top_regions": [], "total_replays": 0, "total_revenue_cents": 0, "visitors": 0, @@ -2279,6 +1914,202 @@ NiceResponse { "recent_emails": [], "total_emails": 0, }, + "hourly_active_users": [ + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + ], + "hourly_users": [ + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + ], "live_users": , "login_methods": [], "payments_overview": { @@ -2462,6 +2293,7 @@ NiceResponse { "analytics_overview": { "anonymous_visitors_fallback": 0, "avg_session_seconds": 0, + "bounce_rate": 0, "daily_anonymous_visitors_fallback": [ { "activity": 0, @@ -2588,258 +2420,10 @@ NiceResponse { "date": , }, ], - "daily_clicks": [ - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - ], - "daily_page_views": [ - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - ], + "daily_avg_session_seconds": [], + "daily_bounce_rate": [], + "daily_clicks": [], + "daily_page_views": [], "daily_revenue": [ { "date": , @@ -2997,141 +2581,19 @@ NiceResponse { "refund_cents": 0, }, ], - "daily_visitors": [ - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - { - "activity": 0, - "date": , - }, - ], - "online_live": 10, + "daily_visitors": [], + "hourly_active_users": [], + "hourly_page_views": [], + "hourly_visitors": [], + "online_live": 0, "recent_replays": 0, "revenue_per_visitor": 0, + "top_browsers": [], + "top_devices": [], + "top_operating_systems": [], "top_referrers": [], - "top_region": { - "count": 10, - "country_code": null, - "region_code": null, - }, + "top_region": null, + "top_regions": [], "total_replays": 0, "total_revenue_cents": 0, "visitors": 0, @@ -4828,6 +4290,202 @@ NiceResponse { ], "total_emails": 15, }, + "hourly_active_users": [ + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 10, + "date": , + }, + ], + "hourly_users": [ + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 0, + "date": , + }, + { + "activity": 9, + "date": , + }, + ], "live_users": , "login_methods": [ { diff --git a/apps/e2e/tests/js/cross-domain-auth.test.ts b/apps/e2e/tests/js/cross-domain-auth.test.ts index 599ba699a..ed970daf6 100644 --- a/apps/e2e/tests/js/cross-domain-auth.test.ts +++ b/apps/e2e/tests/js/cross-domain-auth.test.ts @@ -293,8 +293,8 @@ it("does not await pending auth resolutions when post-callback redirect adds nes await withHostedDomainSuffix(async () => { const projectId = "13131313-1313-4313-8313-131313131313"; const clientApp = createClientApp(projectId); - const getCurrentRefreshTokenIdIfSignedInSpy = vi - .spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn") + const fetchCurrentRefreshTokenIdIfSignedInSpy = vi + .spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn") .mockResolvedValue(null); const previousWindow = globalThis.window; @@ -310,8 +310,10 @@ it("does not await pending auth resolutions when post-callback redirect adds nes } as any; try { + // accountSettings (unlike afterSignIn & co, which resolve to local URLs) still lives on the + // hosted domain, so it exercises the nested cross-domain auth params path. await expect((clientApp as any)._redirectToHandler( - "afterSignIn", + "accountSettings", { replace: true }, { awaitPendingAuthResolutions: false }, )).rejects.toThrowError("INTENTIONAL_TEST_ABORT"); @@ -320,9 +322,9 @@ it("does not await pending auth resolutions when post-callback redirect adds nes globalThis.document = previousDocument; } - expect(getCurrentRefreshTokenIdIfSignedInSpy).toHaveBeenCalledWith({ + expect(fetchCurrentRefreshTokenIdIfSignedInSpy).toHaveBeenCalledWith(expect.objectContaining({ awaitPendingAuthResolutions: false, - }); + })); }); }); @@ -446,7 +448,7 @@ it("adds nested cross-domain auth params when redirecting signed-in users to hos const currentHref = `${localRedirectUrl}/dashboard?tab=settings`; const clientApp = createClientApp(projectId); - vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(refreshTokenId); + vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(refreshTokenId); const previousWindow = globalThis.window; const previousDocument = globalThis.document; @@ -484,7 +486,7 @@ it("adds nested cross-domain auth params for other cross-domain handler redirect const currentHref = `${localRedirectUrl}/private-page`; const clientApp = createClientApp(projectId); - vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(refreshTokenId); + vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(refreshTokenId); const previousWindow = globalThis.window; const previousDocument = globalThis.document; @@ -524,7 +526,7 @@ it("starts nested cross-domain auth from the target domain", async ({ expect }) const previousDocument = globalThis.document; let redirectedUrl = ""; - vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null); + vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null); vi.spyOn(clientApp as any, "_getCrossDomainHandoffParamsForRedirect").mockResolvedValue({ state: "nested-state", codeChallenge: "nested-code-challenge", @@ -588,7 +590,7 @@ it("carries hosted sign-in return state on the nested OAuth redirect URI", async const previousDocument = globalThis.document; let redirectedUrl = ""; - vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null); + vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null); vi.spyOn(clientApp as any, "_getCrossDomainHandoffParamsForRedirect").mockResolvedValue({ state: "nested-state", codeChallenge: "nested-code-challenge", @@ -649,7 +651,7 @@ it("continues nested cross-domain auth on the source domain", async ({ expect }) const createCrossDomainAuthRedirectUrlSpy = vi .spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl") .mockResolvedValue(crossDomainRedirect); - vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(sourceRefreshTokenId); + vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(sourceRefreshTokenId); globalThis.document = createMockDocument(); globalThis.window = { @@ -721,7 +723,7 @@ it("rejects nested cross-domain auth when the callback URL is untrusted", async const previousWindow = globalThis.window; const previousDocument = globalThis.document; - vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null); + vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue(null); vi.spyOn(clientApp as any, "_isTrusted").mockResolvedValue(false); globalThis.document = createMockDocument(); @@ -753,7 +755,7 @@ it("rejects nested cross-domain auth when the source session does not match", as const previousWindow = globalThis.window; const previousDocument = globalThis.document; const createCrossDomainAuthRedirectUrlSpy = vi.spyOn(clientApp as any, "_createCrossDomainAuthRedirectUrl"); - vi.spyOn(clientApp as any, "_getCurrentRefreshTokenIdIfSignedIn").mockResolvedValue("different-source-session"); + vi.spyOn(clientApp as any, "_fetchCurrentRefreshTokenIdIfSignedIn").mockResolvedValue("different-source-session"); globalThis.document = createMockDocument(); globalThis.window = { diff --git a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts index 57fb05809..4d10a7a34 100644 --- a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +++ b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts @@ -283,6 +283,12 @@ describe("StackClientApp cross-domain auth", () => { const originalFetchNewAccessToken = Reflect.get(clientInterface, "fetchNewAccessToken"); const refreshedRawRefreshTokens: string[] = []; + // Cookie-store writes queue a background trusted-parent-domain lookup. Without this stub, that + // lookup fetches the (unreachable) baseUrl with retries while holding the global store lock, + // which starves any later test that needs the write lock (e.g. signOut). Not restored on + // purpose: queued tasks can still run after this test body finishes. + vi.spyOn(clientApp as any, "_getTrustedParentDomain").mockResolvedValue(null); + try { const getBrowserCookieTokenStore = Reflect.get(clientApp, "_getBrowserCookieTokenStore"); if (typeof getBrowserCookieTokenStore !== "function") {