From 2e74a31001a3d3d009f7c97ddb9b8526effb4eb0 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Tue, 26 May 2026 14:25:52 +0100 Subject: [PATCH 1/3] feat(react): add Firebase Analytics hooks with tests Implement Analytics query and mutation hooks aligned with Firestore DX, wire the module for build/export, and add full test coverage. --- packages/react/package.json | 4 + packages/react/src/analytics/index.ts | 13 ++- .../react/src/analytics/queryKeys.test.ts | 19 ++++ packages/react/src/analytics/queryKeys.ts | 22 ++++ packages/react/src/analytics/types.ts | 77 ++++++++++++++ ...seGetGoogleAnalyticsClientIdQuery.test.tsx | 77 ++++++++++++++ .../useGetGoogleAnalyticsClientIdQuery.ts | 42 ++++++++ .../analytics/useIsSupportedQuery.test.tsx | 81 ++++++++++++++ .../src/analytics/useIsSupportedQuery.ts | 37 +++++++ .../analytics/useLogEventMutation.test.tsx | 100 ++++++++++++++++++ .../src/analytics/useLogEventMutation.ts | 56 ++++++++++ ...nalyticsCollectionEnabledMutation.test.tsx | 98 +++++++++++++++++ ...seSetAnalyticsCollectionEnabledMutation.ts | 44 ++++++++ .../analytics/useSetConsentMutation.test.tsx | 67 ++++++++++++ .../src/analytics/useSetConsentMutation.ts | 46 ++++++++ .../useSetCurrentScreenMutation.test.tsx | 81 ++++++++++++++ .../analytics/useSetCurrentScreenMutation.ts | 50 +++++++++ ...SetDefaultEventParametersMutation.test.tsx | 71 +++++++++++++ .../useSetDefaultEventParametersMutation.ts | 42 ++++++++ .../analytics/useSetUserIdMutation.test.tsx | 89 ++++++++++++++++ .../src/analytics/useSetUserIdMutation.ts | 42 ++++++++ .../useSetUserPropertiesMutation.test.tsx | 82 ++++++++++++++ .../analytics/useSetUserPropertiesMutation.ts | 46 ++++++++ packages/react/tsup.config.ts | 2 +- 24 files changed, 1284 insertions(+), 4 deletions(-) create mode 100644 packages/react/src/analytics/queryKeys.test.ts create mode 100644 packages/react/src/analytics/queryKeys.ts create mode 100644 packages/react/src/analytics/types.ts create mode 100644 packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.test.tsx create mode 100644 packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.ts create mode 100644 packages/react/src/analytics/useIsSupportedQuery.test.tsx create mode 100644 packages/react/src/analytics/useIsSupportedQuery.ts create mode 100644 packages/react/src/analytics/useLogEventMutation.test.tsx create mode 100644 packages/react/src/analytics/useLogEventMutation.ts create mode 100644 packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.test.tsx create mode 100644 packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.ts create mode 100644 packages/react/src/analytics/useSetConsentMutation.test.tsx create mode 100644 packages/react/src/analytics/useSetConsentMutation.ts create mode 100644 packages/react/src/analytics/useSetCurrentScreenMutation.test.tsx create mode 100644 packages/react/src/analytics/useSetCurrentScreenMutation.ts create mode 100644 packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx create mode 100644 packages/react/src/analytics/useSetDefaultEventParametersMutation.ts create mode 100644 packages/react/src/analytics/useSetUserIdMutation.test.tsx create mode 100644 packages/react/src/analytics/useSetUserIdMutation.ts create mode 100644 packages/react/src/analytics/useSetUserPropertiesMutation.test.tsx create mode 100644 packages/react/src/analytics/useSetUserPropertiesMutation.ts diff --git a/packages/react/package.json b/packages/react/package.json index cbfbbccb..be0fe0ff 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -18,6 +18,10 @@ "import": "./dist/index.js", "types": "./dist/index.d.ts" }, + "./analytics": { + "import": "./dist/analytics/index.js", + "types": "./dist/analytics/index.d.ts" + }, "./auth": { "import": "./dist/auth/index.js", "types": "./dist/auth/index.d.ts" diff --git a/packages/react/src/analytics/index.ts b/packages/react/src/analytics/index.ts index 7460b873..b8016a48 100644 --- a/packages/react/src/analytics/index.ts +++ b/packages/react/src/analytics/index.ts @@ -1,3 +1,10 @@ -// useGetGoogleAnalyticsClientIdQuery -// useLogEventMutation -// useIsSupportedQuery +export { analyticsQueryKeys } from "./queryKeys"; +export { useGetGoogleAnalyticsClientIdQuery } from "./useGetGoogleAnalyticsClientIdQuery"; +export { useIsSupportedQuery } from "./useIsSupportedQuery"; +export { useLogEventMutation } from "./useLogEventMutation"; +export { useSetAnalyticsCollectionEnabledMutation } from "./useSetAnalyticsCollectionEnabledMutation"; +export { useSetConsentMutation } from "./useSetConsentMutation"; +export { useSetCurrentScreenMutation } from "./useSetCurrentScreenMutation"; +export { useSetDefaultEventParametersMutation } from "./useSetDefaultEventParametersMutation"; +export { useSetUserIdMutation } from "./useSetUserIdMutation"; +export { useSetUserPropertiesMutation } from "./useSetUserPropertiesMutation"; diff --git a/packages/react/src/analytics/queryKeys.test.ts b/packages/react/src/analytics/queryKeys.test.ts new file mode 100644 index 00000000..ef043806 --- /dev/null +++ b/packages/react/src/analytics/queryKeys.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "vitest"; +import { analyticsQueryKeys } from "./queryKeys"; + +describe("analyticsQueryKeys", () => { + test("builds a google analytics client id key from app name", () => { + expect(analyticsQueryKeys.googleAnalyticsClientId("my-app")).toEqual([ + "analytics", + "googleAnalyticsClientId", + "my-app", + ]); + }); + + test("builds an isSupported key", () => { + expect(analyticsQueryKeys.isSupported()).toEqual([ + "analytics", + "isSupported", + ]); + }); +}); diff --git a/packages/react/src/analytics/queryKeys.ts b/packages/react/src/analytics/queryKeys.ts new file mode 100644 index 00000000..db96836c --- /dev/null +++ b/packages/react/src/analytics/queryKeys.ts @@ -0,0 +1,22 @@ +/** + * Optional query key helpers for Analytics hooks. + * + * Analytics query hooks require a caller-provided `queryKey` (same as Firestore). + * Use these factories to build consistent keys for invalidation and deduplication. + * + * @example + * useGetGoogleAnalyticsClientIdQuery(analytics, { + * queryKey: analyticsQueryKeys.googleAnalyticsClientId(analytics.app.name), + * }); + * + * @example + * useIsSupportedQuery({ + * queryKey: analyticsQueryKeys.isSupported(), + * }); + */ +export const analyticsQueryKeys = { + all: ["analytics"] as const, + googleAnalyticsClientId: (appName: string) => + [...analyticsQueryKeys.all, "googleAnalyticsClientId", appName] as const, + isSupported: () => [...analyticsQueryKeys.all, "isSupported"] as const, +}; diff --git a/packages/react/src/analytics/types.ts b/packages/react/src/analytics/types.ts new file mode 100644 index 00000000..44ba57c3 --- /dev/null +++ b/packages/react/src/analytics/types.ts @@ -0,0 +1,77 @@ +import type { UseMutationOptions, UseQueryOptions } from "@tanstack/react-query"; +import type { + AnalyticsCallOptions, + ConsentSettings, + CustomEventName, + CustomParams, + EventNameString, + EventParams, +} from "firebase/analytics"; + +/** + * TanStack Query options for Analytics read hooks. + * Caller must provide `queryKey` (same as Firestore hooks). + * Firebase Analytics read APIs expose no additional query parameters beyond + * the `Analytics` instance passed to the hook. + */ +export type AnalyticsUseQueryOptions = Omit< + UseQueryOptions, + "queryFn" +>; + +export type AnalyticsUseMutationOptions< + TData = unknown, + TError = Error, + TVariables = void, +> = Omit, "mutationFn">; + +/** + * Variables for {@link useLogEventMutation}. + * Mirrors `logEvent(analyticsInstance, eventName, eventParams?, callOptions?)`. + */ +export type LogEventVariables = { + eventName: EventNameString | CustomEventName; + eventParams?: EventParams; + callOptions?: AnalyticsCallOptions; +}; + +/** + * Variables for {@link useSetUserIdMutation}. + * Mirrors `setUserId(analyticsInstance, id, callOptions?)`. + */ +export type SetUserIdVariables = { + id: string | null; + callOptions?: AnalyticsCallOptions; +}; + +/** + * Variables for {@link useSetUserPropertiesMutation}. + * Mirrors `setUserProperties(analyticsInstance, properties, callOptions?)`. + */ +export type SetUserPropertiesVariables = { + properties: CustomParams; + callOptions?: AnalyticsCallOptions; +}; + +/** + * Variables for {@link useSetCurrentScreenMutation}. + * Mirrors `setCurrentScreen(analyticsInstance, screenName, callOptions?)`. + * + * @deprecated Prefer {@link useLogEventMutation} with `eventName: 'screen_view'`. + */ +export type SetCurrentScreenVariables = { + screenName: string; + callOptions?: AnalyticsCallOptions; +}; + +/** + * Variables for {@link useSetConsentMutation}. + * Mirrors `setConsent(consentSettings)`. + */ +export type SetConsentVariables = ConsentSettings; + +/** + * Variables for {@link useSetDefaultEventParametersMutation}. + * Mirrors `setDefaultEventParameters(customParams)`. + */ +export type SetDefaultEventParametersVariables = CustomParams; diff --git a/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.test.tsx b/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.test.tsx new file mode 100644 index 00000000..25621bd1 --- /dev/null +++ b/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.test.tsx @@ -0,0 +1,77 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import type { Analytics } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; +import { getGoogleAnalyticsClientId } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { analyticsQueryKeys } from "./queryKeys"; +import { useGetGoogleAnalyticsClientIdQuery } from "./useGetGoogleAnalyticsClientIdQuery"; + +vi.mock("firebase/analytics", () => ({ + getGoogleAnalyticsClientId: vi.fn(), +})); + +const mockAnalytics = { + app: { name: "[DEFAULT]" } as FirebaseApp, +} as Analytics; + +describe("useGetGoogleAnalyticsClientIdQuery", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("successfully retrieves a Google Analytics client id", async () => { + vi.mocked(getGoogleAnalyticsClientId).mockResolvedValueOnce("client-123"); + + const { result } = renderHook( + () => + useGetGoogleAnalyticsClientIdQuery(mockAnalytics, { + queryKey: analyticsQueryKeys.googleAnalyticsClientId( + mockAnalytics.app.name, + ), + }), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBe("client-123"); + expect(getGoogleAnalyticsClientId).toHaveBeenCalledWith(mockAnalytics); + }); + + test("handles retrieval failure", async () => { + const error = new Error("Failed to get client id"); + vi.mocked(getGoogleAnalyticsClientId).mockRejectedValueOnce(error); + + const { result } = renderHook( + () => + useGetGoogleAnalyticsClientIdQuery(mockAnalytics, { + queryKey: analyticsQueryKeys.googleAnalyticsClientId( + mockAnalytics.app.name, + ), + }), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); + + test("respects enabled option", async () => { + const { result } = renderHook( + () => + useGetGoogleAnalyticsClientIdQuery(mockAnalytics, { + queryKey: analyticsQueryKeys.googleAnalyticsClientId( + mockAnalytics.app.name, + ), + enabled: false, + }), + { wrapper }, + ); + + expect(result.current.status).toBe("pending"); + expect(getGoogleAnalyticsClientId).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.ts b/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.ts new file mode 100644 index 00000000..a72e2537 --- /dev/null +++ b/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.ts @@ -0,0 +1,42 @@ +import { useQuery } from "@tanstack/react-query"; +import { type Analytics, getGoogleAnalyticsClientId } from "firebase/analytics"; +import type { AnalyticsUseQueryOptions } from "./types"; + +/** + * Hook to retrieve the Google Analytics client ID for a Firebase Analytics instance. + * + * Wraps {@link getGoogleAnalyticsClientId}. The Firebase API accepts only an + * `Analytics` instance and returns `Promise` — there are no additional + * read options to configure (unlike Firestore's server/cache source selector). + * + * @param analytics - Firebase Analytics instance from `getAnalytics()` or `initializeAnalytics()` + * @param options - TanStack Query options; `queryKey` is required + * @returns TanStack Query result with the client ID string + * + * @example + * import { getAnalytics } from 'firebase/analytics'; + * import { analyticsQueryKeys, useGetGoogleAnalyticsClientIdQuery } from '@tanstack-query-firebase/react/analytics'; + * + * const analytics = getAnalytics(app); + * + * const { data: clientId, isLoading } = useGetGoogleAnalyticsClientIdQuery( + * analytics, + * { queryKey: analyticsQueryKeys.googleAnalyticsClientId(analytics.app.name) }, + * ); + * + * @example + * // Disable until analytics is initialized + * const { data: clientId } = useGetGoogleAnalyticsClientIdQuery(analytics, { + * queryKey: analyticsQueryKeys.googleAnalyticsClientId(analytics.app.name), + * enabled: !!analytics, + * }); + */ +export function useGetGoogleAnalyticsClientIdQuery( + analytics: Analytics, + options: AnalyticsUseQueryOptions, +) { + return useQuery({ + ...options, + queryFn: () => getGoogleAnalyticsClientId(analytics), + }); +} diff --git a/packages/react/src/analytics/useIsSupportedQuery.test.tsx b/packages/react/src/analytics/useIsSupportedQuery.test.tsx new file mode 100644 index 00000000..0f3e778c --- /dev/null +++ b/packages/react/src/analytics/useIsSupportedQuery.test.tsx @@ -0,0 +1,81 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { isSupported } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { analyticsQueryKeys } from "./queryKeys"; +import { useIsSupportedQuery } from "./useIsSupportedQuery"; + +vi.mock("firebase/analytics", () => ({ + isSupported: vi.fn(), +})); + +describe("useIsSupportedQuery", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("returns true when analytics is supported", async () => { + vi.mocked(isSupported).mockResolvedValueOnce(true); + + const { result } = renderHook( + () => + useIsSupportedQuery({ + queryKey: analyticsQueryKeys.isSupported(), + }), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBe(true); + expect(isSupported).toHaveBeenCalledTimes(1); + }); + + test("returns false when analytics is not supported", async () => { + vi.mocked(isSupported).mockResolvedValueOnce(false); + + const { result } = renderHook( + () => + useIsSupportedQuery({ + queryKey: analyticsQueryKeys.isSupported(), + }), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBe(false); + }); + + test("handles check failure", async () => { + const error = new Error("Support check failed"); + vi.mocked(isSupported).mockRejectedValueOnce(error); + + const { result } = renderHook( + () => + useIsSupportedQuery({ + queryKey: analyticsQueryKeys.isSupported(), + }), + { wrapper }, + ); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); + + test("respects enabled option", async () => { + const { result } = renderHook( + () => + useIsSupportedQuery({ + queryKey: analyticsQueryKeys.isSupported(), + enabled: false, + }), + { wrapper }, + ); + + expect(result.current.status).toBe("pending"); + expect(isSupported).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/react/src/analytics/useIsSupportedQuery.ts b/packages/react/src/analytics/useIsSupportedQuery.ts new file mode 100644 index 00000000..dd7d189e --- /dev/null +++ b/packages/react/src/analytics/useIsSupportedQuery.ts @@ -0,0 +1,37 @@ +import { useQuery } from "@tanstack/react-query"; +import { isSupported } from "firebase/analytics"; +import type { AnalyticsUseQueryOptions } from "./types"; + +/** + * Hook to check whether Firebase Analytics is supported in the current environment. + * + * Wraps {@link isSupported}. The Firebase API takes no parameters and returns + * `Promise` — there are no additional read options to configure. + * + * @param options - TanStack Query options; `queryKey` is required + * @returns TanStack Query result with the support check boolean + * + * @example + * import { analyticsQueryKeys, useIsSupportedQuery } from '@tanstack-query-firebase/react/analytics'; + * + * const { data: supported, isLoading } = useIsSupportedQuery({ + * queryKey: analyticsQueryKeys.isSupported(), + * }); + * + * if (supported) { + * // Safe to call getAnalytics() + * } + * + * @example + * // Gate analytics initialization + * const { data: supported } = useIsSupportedQuery({ + * queryKey: analyticsQueryKeys.isSupported(), + * staleTime: Number.POSITIVE_INFINITY, + * }); + */ +export function useIsSupportedQuery(options: AnalyticsUseQueryOptions) { + return useQuery({ + ...options, + queryFn: () => isSupported(), + }); +} diff --git a/packages/react/src/analytics/useLogEventMutation.test.tsx b/packages/react/src/analytics/useLogEventMutation.test.tsx new file mode 100644 index 00000000..8c48586d --- /dev/null +++ b/packages/react/src/analytics/useLogEventMutation.test.tsx @@ -0,0 +1,100 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import type { Analytics } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; +import { logEvent } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { useLogEventMutation } from "./useLogEventMutation"; + +vi.mock("firebase/analytics", () => ({ + logEvent: vi.fn(), +})); + +const mockAnalytics = { + app: { name: "[DEFAULT]" } as FirebaseApp, +} as Analytics; + +describe("useLogEventMutation", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("successfully logs an event", async () => { + const { result } = renderHook(() => useLogEventMutation(mockAnalytics), { + wrapper, + }); + + await act(async () => { + result.current.mutate({ + eventName: "login", + eventParams: { method: "email" }, + }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(logEvent).toHaveBeenCalledWith(mockAnalytics, "login", { + method: "email", + }, undefined); + }); + + test("passes analytics call options", async () => { + const { result } = renderHook(() => useLogEventMutation(mockAnalytics), { + wrapper, + }); + + await act(async () => { + result.current.mutate({ + eventName: "page_view", + eventParams: { page_title: "Home" }, + callOptions: { global: true }, + }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(logEvent).toHaveBeenCalledWith( + mockAnalytics, + "page_view", + { page_title: "Home" }, + { global: true }, + ); + }); + + test("handles log event failure", async () => { + const error = new Error("Failed to log event"); + vi.mocked(logEvent).mockImplementationOnce(() => { + throw error; + }); + + const { result } = renderHook(() => useLogEventMutation(mockAnalytics), { + wrapper, + }); + + await act(async () => { + result.current.mutate({ eventName: "login" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); + + test("calls onSuccess callback after successful log", async () => { + const onSuccessMock = vi.fn(); + + const { result } = renderHook( + () => useLogEventMutation(mockAnalytics, { onSuccess: onSuccessMock }), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ eventName: "sign_up" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(onSuccessMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/react/src/analytics/useLogEventMutation.ts b/packages/react/src/analytics/useLogEventMutation.ts new file mode 100644 index 00000000..1673699e --- /dev/null +++ b/packages/react/src/analytics/useLogEventMutation.ts @@ -0,0 +1,56 @@ +import { useMutation } from "@tanstack/react-query"; +import { type Analytics, logEvent } from "firebase/analytics"; +import type { AnalyticsUseMutationOptions, LogEventVariables } from "./types"; + +/** + * Hook to log a Google Analytics event via Firebase Analytics. + * + * Wraps {@link logEvent}. Pass event details as mutation variables; each field + * maps directly to the Firebase API: + * `logEvent(analyticsInstance, eventName, eventParams?, callOptions?)`. + * + * @param analytics - Firebase Analytics instance from `getAnalytics()` or `initializeAnalytics()` + * @param options - TanStack Query mutation options + * @returns TanStack Query mutation result + * + * @example + * import { getAnalytics, logEvent } from 'firebase/analytics'; + * import { useLogEventMutation } from '@tanstack-query-firebase/react/analytics'; + * + * const analytics = getAnalytics(app); + * const { mutate: logAnalyticsEvent } = useLogEventMutation(analytics); + * + * logAnalyticsEvent({ + * eventName: 'login', + * eventParams: { method: 'email' }, + * }); + * + * @example + * // Recommended screen tracking (replaces deprecated setCurrentScreen) + * logAnalyticsEvent({ + * eventName: 'screen_view', + * eventParams: { + * firebase_screen: 'Home', + * firebase_screen_class: 'HomeScreen', + * }, + * }); + * + * @example + * // Apply to all Google Analytics properties on the page + * logAnalyticsEvent({ + * eventName: 'purchase', + * eventParams: { transaction_id: 'T123', value: 9.99, currency: 'USD' }, + * callOptions: { global: true }, + * }); + */ +export function useLogEventMutation( + analytics: Analytics, + options?: AnalyticsUseMutationOptions, +) { + return useMutation({ + ...options, + mutationFn: async ({ eventName, eventParams, callOptions }) => { + logEvent(analytics, eventName, eventParams, callOptions); + }, + }); +} diff --git a/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.test.tsx b/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.test.tsx new file mode 100644 index 00000000..7d31e49c --- /dev/null +++ b/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.test.tsx @@ -0,0 +1,98 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import type { Analytics } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; +import { setAnalyticsCollectionEnabled } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { useSetAnalyticsCollectionEnabledMutation } from "./useSetAnalyticsCollectionEnabledMutation"; + +vi.mock("firebase/analytics", () => ({ + setAnalyticsCollectionEnabled: vi.fn(), +})); + +const mockAnalytics = { + app: { name: "[DEFAULT]" } as FirebaseApp, +} as Analytics; + +describe("useSetAnalyticsCollectionEnabledMutation", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("successfully enables analytics collection", async () => { + const { result } = renderHook( + () => useSetAnalyticsCollectionEnabledMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate(true); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setAnalyticsCollectionEnabled).toHaveBeenCalledWith( + mockAnalytics, + true, + ); + }); + + test("successfully disables analytics collection", async () => { + const { result } = renderHook( + () => useSetAnalyticsCollectionEnabledMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate(false); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setAnalyticsCollectionEnabled).toHaveBeenCalledWith( + mockAnalytics, + false, + ); + }); + + test("handles failure", async () => { + const error = new Error("Failed to set collection enabled"); + vi.mocked(setAnalyticsCollectionEnabled).mockImplementationOnce(() => { + throw error; + }); + + const { result } = renderHook( + () => useSetAnalyticsCollectionEnabledMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate(true); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); + + test("calls onSuccess callback after successful update", async () => { + const onSuccessMock = vi.fn(); + + const { result } = renderHook( + () => + useSetAnalyticsCollectionEnabledMutation(mockAnalytics, { + onSuccess: onSuccessMock, + }), + { wrapper }, + ); + + await act(async () => { + result.current.mutate(true); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(onSuccessMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.ts b/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.ts new file mode 100644 index 00000000..f29d9b1f --- /dev/null +++ b/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.ts @@ -0,0 +1,44 @@ +import { useMutation } from "@tanstack/react-query"; +import { + type Analytics, + setAnalyticsCollectionEnabled, +} from "firebase/analytics"; +import type { AnalyticsUseMutationOptions } from "./types"; + +/** + * Hook to enable or disable Google Analytics collection for an app on the current device. + * + * Wraps {@link setAnalyticsCollectionEnabled}. The mutation variable is the + * `enabled` boolean passed directly to the Firebase API: + * `setAnalyticsCollectionEnabled(analyticsInstance, enabled)`. + * + * @param analytics - Firebase Analytics instance from `getAnalytics()` or `initializeAnalytics()` + * @param options - TanStack Query mutation options + * @returns TanStack Query mutation result + * + * @example + * import { getAnalytics } from 'firebase/analytics'; + * import { useSetAnalyticsCollectionEnabledMutation } from '@tanstack-query-firebase/react/analytics'; + * + * const analytics = getAnalytics(app); + * const { mutate: setCollectionEnabled } = + * useSetAnalyticsCollectionEnabledMutation(analytics); + * + * // Opt user out of analytics collection + * setCollectionEnabled(false); + * + * @example + * // Re-enable after consent is granted + * setCollectionEnabled(true); + */ +export function useSetAnalyticsCollectionEnabledMutation( + analytics: Analytics, + options?: AnalyticsUseMutationOptions, +) { + return useMutation({ + ...options, + mutationFn: async (enabled) => { + setAnalyticsCollectionEnabled(analytics, enabled); + }, + }); +} diff --git a/packages/react/src/analytics/useSetConsentMutation.test.tsx b/packages/react/src/analytics/useSetConsentMutation.test.tsx new file mode 100644 index 00000000..3425d51c --- /dev/null +++ b/packages/react/src/analytics/useSetConsentMutation.test.tsx @@ -0,0 +1,67 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import { setConsent } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { useSetConsentMutation } from "./useSetConsentMutation"; + +vi.mock("firebase/analytics", () => ({ + setConsent: vi.fn(), +})); + +describe("useSetConsentMutation", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("successfully sets consent settings", async () => { + const consentSettings = { + analytics_storage: "granted" as const, + ad_storage: "denied" as const, + }; + + const { result } = renderHook(() => useSetConsentMutation(), { wrapper }); + + await act(async () => { + result.current.mutate(consentSettings); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setConsent).toHaveBeenCalledWith(consentSettings); + }); + + test("handles failure", async () => { + const error = new Error("Failed to set consent"); + vi.mocked(setConsent).mockImplementationOnce(() => { + throw error; + }); + + const { result } = renderHook(() => useSetConsentMutation(), { wrapper }); + + await act(async () => { + result.current.mutate({ analytics_storage: "granted" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); + + test("calls onSuccess callback after successful update", async () => { + const onSuccessMock = vi.fn(); + + const { result } = renderHook( + () => useSetConsentMutation({ onSuccess: onSuccessMock }), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ analytics_storage: "granted" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(onSuccessMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/react/src/analytics/useSetConsentMutation.ts b/packages/react/src/analytics/useSetConsentMutation.ts new file mode 100644 index 00000000..2d583577 --- /dev/null +++ b/packages/react/src/analytics/useSetConsentMutation.ts @@ -0,0 +1,46 @@ +import { useMutation } from "@tanstack/react-query"; +import { setConsent } from "firebase/analytics"; +import type { + AnalyticsUseMutationOptions, + SetConsentVariables, +} from "./types"; + +/** + * Hook to set end-user consent state for Google Analytics across all gtag references. + * + * Wraps {@link setConsent}. This is a module-level Firebase API (no `Analytics` + * instance). The mutation variable is the `ConsentSettings` object passed + * directly to the Firebase API: `setConsent(consentSettings)`. + * + * @param options - TanStack Query mutation options + * @returns TanStack Query mutation result + * + * @example + * import { useSetConsentMutation } from '@tanstack-query-firebase/react/analytics'; + * + * const { mutate: setAnalyticsConsent } = useSetConsentMutation(); + * + * setAnalyticsConsent({ + * analytics_storage: 'granted', + * ad_storage: 'denied', + * }); + * + * @example + * // Update consent after the user accepts a CMP banner + * setAnalyticsConsent({ + * analytics_storage: 'granted', + * ad_storage: 'granted', + * ad_user_data: 'granted', + * ad_personalization: 'denied', + * }); + */ +export function useSetConsentMutation( + options?: AnalyticsUseMutationOptions, +) { + return useMutation({ + ...options, + mutationFn: async (consentSettings) => { + setConsent(consentSettings); + }, + }); +} diff --git a/packages/react/src/analytics/useSetCurrentScreenMutation.test.tsx b/packages/react/src/analytics/useSetCurrentScreenMutation.test.tsx new file mode 100644 index 00000000..1cb06fa6 --- /dev/null +++ b/packages/react/src/analytics/useSetCurrentScreenMutation.test.tsx @@ -0,0 +1,81 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import type { Analytics } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; +import { setCurrentScreen } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { useSetCurrentScreenMutation } from "./useSetCurrentScreenMutation"; + +vi.mock("firebase/analytics", () => ({ + setCurrentScreen: vi.fn(), +})); + +const mockAnalytics = { + app: { name: "[DEFAULT]" } as FirebaseApp, +} as Analytics; + +describe("useSetCurrentScreenMutation", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("successfully sets the current screen", async () => { + const { result } = renderHook( + () => useSetCurrentScreenMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ screenName: "Home" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setCurrentScreen).toHaveBeenCalledWith( + mockAnalytics, + "Home", + undefined, + ); + }); + + test("passes analytics call options", async () => { + const { result } = renderHook( + () => useSetCurrentScreenMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ + screenName: "Settings", + callOptions: { global: true }, + }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setCurrentScreen).toHaveBeenCalledWith(mockAnalytics, "Settings", { + global: true, + }); + }); + + test("handles failure", async () => { + const error = new Error("Failed to set current screen"); + vi.mocked(setCurrentScreen).mockImplementationOnce(() => { + throw error; + }); + + const { result } = renderHook( + () => useSetCurrentScreenMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ screenName: "Home" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); +}); diff --git a/packages/react/src/analytics/useSetCurrentScreenMutation.ts b/packages/react/src/analytics/useSetCurrentScreenMutation.ts new file mode 100644 index 00000000..72d76177 --- /dev/null +++ b/packages/react/src/analytics/useSetCurrentScreenMutation.ts @@ -0,0 +1,50 @@ +import { useMutation } from "@tanstack/react-query"; +import { type Analytics, setCurrentScreen } from "firebase/analytics"; +import type { + AnalyticsUseMutationOptions, + SetCurrentScreenVariables, +} from "./types"; + +/** + * Hook to set the current screen name via gtag config. + * + * Wraps {@link setCurrentScreen}. Mutation variables mirror the Firebase API: + * `setCurrentScreen(analyticsInstance, screenName, callOptions?)`. + * + * @deprecated Prefer {@link useLogEventMutation} with `eventName: 'screen_view'` + * and `eventParams.firebase_screen` / `eventParams.firebase_screen_class`. + * + * @param analytics - Firebase Analytics instance from `getAnalytics()` or `initializeAnalytics()` + * @param options - TanStack Query mutation options + * @returns TanStack Query mutation result + * + * @example + * import { getAnalytics } from 'firebase/analytics'; + * import { useSetCurrentScreenMutation } from '@tanstack-query-firebase/react/analytics'; + * + * const analytics = getAnalytics(app); + * const { mutate: setScreen } = useSetCurrentScreenMutation(analytics); + * + * setScreen({ screenName: 'Home' }); + * + * @example + * // Prefer screen_view events for new code + * import { useLogEventMutation } from '@tanstack-query-firebase/react/analytics'; + * + * const { mutate: logEvent } = useLogEventMutation(analytics); + * logEvent({ + * eventName: 'screen_view', + * eventParams: { firebase_screen: 'Home' }, + * }); + */ +export function useSetCurrentScreenMutation( + analytics: Analytics, + options?: AnalyticsUseMutationOptions, +) { + return useMutation({ + ...options, + mutationFn: async ({ screenName, callOptions }) => { + setCurrentScreen(analytics, screenName, callOptions); + }, + }); +} diff --git a/packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx b/packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx new file mode 100644 index 00000000..adbfc486 --- /dev/null +++ b/packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx @@ -0,0 +1,71 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import { setDefaultEventParameters } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { useSetDefaultEventParametersMutation } from "./useSetDefaultEventParametersMutation"; + +vi.mock("firebase/analytics", () => ({ + setDefaultEventParameters: vi.fn(), +})); + +describe("useSetDefaultEventParametersMutation", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("successfully sets default event parameters", async () => { + const customParams = { session_id: "abc123", debug_mode: true }; + + const { result } = renderHook( + () => useSetDefaultEventParametersMutation(), + { wrapper }, + ); + + await act(async () => { + result.current.mutate(customParams); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setDefaultEventParameters).toHaveBeenCalledWith(customParams); + }); + + test("handles failure", async () => { + const error = new Error("Failed to set default event parameters"); + vi.mocked(setDefaultEventParameters).mockImplementationOnce(() => { + throw error; + }); + + const { result } = renderHook( + () => useSetDefaultEventParametersMutation(), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ session_id: "abc123" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); + + test("calls onSuccess callback after successful update", async () => { + const onSuccessMock = vi.fn(); + + const { result } = renderHook( + () => + useSetDefaultEventParametersMutation({ onSuccess: onSuccessMock }), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ session_id: "abc123" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(onSuccessMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/react/src/analytics/useSetDefaultEventParametersMutation.ts b/packages/react/src/analytics/useSetDefaultEventParametersMutation.ts new file mode 100644 index 00000000..80409ace --- /dev/null +++ b/packages/react/src/analytics/useSetDefaultEventParametersMutation.ts @@ -0,0 +1,42 @@ +import { useMutation } from "@tanstack/react-query"; +import { setDefaultEventParameters } from "firebase/analytics"; +import type { + AnalyticsUseMutationOptions, + SetDefaultEventParametersVariables, +} from "./types"; + +/** + * Hook to set default event parameters applied to every Analytics event on the page. + * + * Wraps {@link setDefaultEventParameters}. This is a module-level Firebase API + * (no `Analytics` instance). The mutation variable is the `CustomParams` object + * passed directly to the Firebase API: `setDefaultEventParameters(customParams)`. + * + * @param options - TanStack Query mutation options + * @returns TanStack Query mutation result + * + * @example + * import { useSetDefaultEventParametersMutation } from '@tanstack-query-firebase/react/analytics'; + * + * const { mutate: setDefaultParams } = useSetDefaultEventParametersMutation(); + * + * setDefaultParams({ session_id: 'abc123', debug_mode: true }); + * + * @example + * // Parameters persist for subsequent automatic and manual events on this page + * setDefaultParams({ campaign: 'spring_sale' }); + */ +export function useSetDefaultEventParametersMutation( + options?: AnalyticsUseMutationOptions< + void, + Error, + SetDefaultEventParametersVariables + >, +) { + return useMutation({ + ...options, + mutationFn: async (customParams) => { + setDefaultEventParameters(customParams); + }, + }); +} diff --git a/packages/react/src/analytics/useSetUserIdMutation.test.tsx b/packages/react/src/analytics/useSetUserIdMutation.test.tsx new file mode 100644 index 00000000..2ca03507 --- /dev/null +++ b/packages/react/src/analytics/useSetUserIdMutation.test.tsx @@ -0,0 +1,89 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import type { Analytics } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; +import { setUserId } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { useSetUserIdMutation } from "./useSetUserIdMutation"; + +vi.mock("firebase/analytics", () => ({ + setUserId: vi.fn(), +})); + +const mockAnalytics = { + app: { name: "[DEFAULT]" } as FirebaseApp, +} as Analytics; + +describe("useSetUserIdMutation", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("successfully sets a user id", async () => { + const { result } = renderHook(() => useSetUserIdMutation(mockAnalytics), { + wrapper, + }); + + await act(async () => { + result.current.mutate({ id: "user-123" }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setUserId).toHaveBeenCalledWith( + mockAnalytics, + "user-123", + undefined, + ); + }); + + test("successfully clears a user id", async () => { + const { result } = renderHook(() => useSetUserIdMutation(mockAnalytics), { + wrapper, + }); + + await act(async () => { + result.current.mutate({ id: null }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setUserId).toHaveBeenCalledWith(mockAnalytics, null, undefined); + }); + + test("passes analytics call options", async () => { + const { result } = renderHook(() => useSetUserIdMutation(mockAnalytics), { + wrapper, + }); + + await act(async () => { + result.current.mutate({ id: "user-123", callOptions: { global: true } }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setUserId).toHaveBeenCalledWith(mockAnalytics, "user-123", { + global: true, + }); + }); + + test("handles failure", async () => { + const error = new Error("Failed to set user id"); + vi.mocked(setUserId).mockImplementationOnce(() => { + throw error; + }); + + const { result } = renderHook(() => useSetUserIdMutation(mockAnalytics), { + wrapper, + }); + + await act(async () => { + result.current.mutate({ id: "user-123" }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); +}); diff --git a/packages/react/src/analytics/useSetUserIdMutation.ts b/packages/react/src/analytics/useSetUserIdMutation.ts new file mode 100644 index 00000000..3b2840dd --- /dev/null +++ b/packages/react/src/analytics/useSetUserIdMutation.ts @@ -0,0 +1,42 @@ +import { useMutation } from "@tanstack/react-query"; +import { type Analytics, setUserId } from "firebase/analytics"; +import type { AnalyticsUseMutationOptions, SetUserIdVariables } from "./types"; + +/** + * Hook to set or clear the Google Analytics user ID for a Firebase Analytics instance. + * + * Wraps {@link setUserId}. Mutation variables mirror the Firebase API: + * `setUserId(analyticsInstance, id, callOptions?)`. + * + * @param analytics - Firebase Analytics instance from `getAnalytics()` or `initializeAnalytics()` + * @param options - TanStack Query mutation options + * @returns TanStack Query mutation result + * + * @example + * import { getAnalytics } from 'firebase/analytics'; + * import { useSetUserIdMutation } from '@tanstack-query-firebase/react/analytics'; + * + * const analytics = getAnalytics(app); + * const { mutate: setAnalyticsUserId } = useSetUserIdMutation(analytics); + * + * setAnalyticsUserId({ id: user.uid }); + * + * @example + * // Clear user ID on sign-out + * setAnalyticsUserId({ id: null }); + * + * @example + * // Apply user ID globally across all gtag properties on the page + * setAnalyticsUserId({ id: user.uid, callOptions: { global: true } }); + */ +export function useSetUserIdMutation( + analytics: Analytics, + options?: AnalyticsUseMutationOptions, +) { + return useMutation({ + ...options, + mutationFn: async ({ id, callOptions }) => { + setUserId(analytics, id, callOptions); + }, + }); +} diff --git a/packages/react/src/analytics/useSetUserPropertiesMutation.test.tsx b/packages/react/src/analytics/useSetUserPropertiesMutation.test.tsx new file mode 100644 index 00000000..9d293937 --- /dev/null +++ b/packages/react/src/analytics/useSetUserPropertiesMutation.test.tsx @@ -0,0 +1,82 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import type { Analytics } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; +import { setUserProperties } from "firebase/analytics"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { queryClient, wrapper } from "../../utils"; +import { useSetUserPropertiesMutation } from "./useSetUserPropertiesMutation"; + +vi.mock("firebase/analytics", () => ({ + setUserProperties: vi.fn(), +})); + +const mockAnalytics = { + app: { name: "[DEFAULT]" } as FirebaseApp, +} as Analytics; + +describe("useSetUserPropertiesMutation", () => { + beforeEach(() => { + vi.clearAllMocks(); + queryClient.clear(); + }); + + test("successfully sets user properties", async () => { + const properties = { favorite_food: "pizza", plan: "premium" }; + + const { result } = renderHook( + () => useSetUserPropertiesMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ properties }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setUserProperties).toHaveBeenCalledWith( + mockAnalytics, + properties, + undefined, + ); + }); + + test("passes analytics call options", async () => { + const properties = { role: "admin" }; + + const { result } = renderHook( + () => useSetUserPropertiesMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ properties, callOptions: { global: true } }); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(setUserProperties).toHaveBeenCalledWith(mockAnalytics, properties, { + global: true, + }); + }); + + test("handles failure", async () => { + const error = new Error("Failed to set user properties"); + vi.mocked(setUserProperties).mockImplementationOnce(() => { + throw error; + }); + + const { result } = renderHook( + () => useSetUserPropertiesMutation(mockAnalytics), + { wrapper }, + ); + + await act(async () => { + result.current.mutate({ properties: { role: "admin" } }); + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error).toBe(error); + }); +}); diff --git a/packages/react/src/analytics/useSetUserPropertiesMutation.ts b/packages/react/src/analytics/useSetUserPropertiesMutation.ts new file mode 100644 index 00000000..4eb45cd8 --- /dev/null +++ b/packages/react/src/analytics/useSetUserPropertiesMutation.ts @@ -0,0 +1,46 @@ +import { useMutation } from "@tanstack/react-query"; +import { type Analytics, setUserProperties } from "firebase/analytics"; +import type { + AnalyticsUseMutationOptions, + SetUserPropertiesVariables, +} from "./types"; + +/** + * Hook to set Google Analytics user properties for a Firebase Analytics instance. + * + * Wraps {@link setUserProperties}. Mutation variables mirror the Firebase API: + * `setUserProperties(analyticsInstance, properties, callOptions?)`. + * + * @param analytics - Firebase Analytics instance from `getAnalytics()` or `initializeAnalytics()` + * @param options - TanStack Query mutation options + * @returns TanStack Query mutation result + * + * @example + * import { getAnalytics } from 'firebase/analytics'; + * import { useSetUserPropertiesMutation } from '@tanstack-query-firebase/react/analytics'; + * + * const analytics = getAnalytics(app); + * const { mutate: setAnalyticsUserProperties } = + * useSetUserPropertiesMutation(analytics); + * + * setAnalyticsUserProperties({ + * properties: { plan: 'premium', role: 'admin' }, + * }); + * + * @example + * setAnalyticsUserProperties({ + * properties: { plan: 'free' }, + * callOptions: { global: true }, + * }); + */ +export function useSetUserPropertiesMutation( + analytics: Analytics, + options?: AnalyticsUseMutationOptions, +) { + return useMutation({ + ...options, + mutationFn: async ({ properties, callOptions }) => { + setUserProperties(analytics, properties, callOptions); + }, + }); +} diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index da3647bf..e38fb322 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs/promises"; import { defineConfig } from "tsup"; -const supportedPackages = ["data-connect", "firestore", "auth"]; +const supportedPackages = ["analytics", "data-connect", "firestore", "auth"]; export default defineConfig({ entry: [`src/(${supportedPackages.join("|")})/index.ts`, "src/index.ts"], format: ["esm"], From 1e2c77ec4adf339d5729a64486f7ad1a554e0243 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Wed, 27 May 2026 09:06:20 +0100 Subject: [PATCH 2/3] docs(react): add Firebase Analytics documentation Document analytics hooks and add sidebar navigation for the new module. --- docs.json | 51 +++++++++ .../useGetGoogleAnalyticsClientIdQuery.mdx | 30 +++++ .../analytics/hooks/useIsSupportedQuery.mdx | 29 +++++ .../analytics/hooks/useLogEventMutation.mdx | 46 ++++++++ ...eSetAnalyticsCollectionEnabledMutation.mdx | 22 ++++ .../analytics/hooks/useSetConsentMutation.mdx | 23 ++++ .../hooks/useSetCurrentScreenMutation.mdx | 34 ++++++ .../useSetDefaultEventParametersMutation.mdx | 22 ++++ .../analytics/hooks/useSetUserIdMutation.mdx | 28 +++++ .../hooks/useSetUserPropertiesMutation.mdx | 31 +++++ docs/react/analytics/index.mdx | 108 ++++++++++++++++++ docs/react/index.mdx | 4 +- 12 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 docs/react/analytics/hooks/useGetGoogleAnalyticsClientIdQuery.mdx create mode 100644 docs/react/analytics/hooks/useIsSupportedQuery.mdx create mode 100644 docs/react/analytics/hooks/useLogEventMutation.mdx create mode 100644 docs/react/analytics/hooks/useSetAnalyticsCollectionEnabledMutation.mdx create mode 100644 docs/react/analytics/hooks/useSetConsentMutation.mdx create mode 100644 docs/react/analytics/hooks/useSetCurrentScreenMutation.mdx create mode 100644 docs/react/analytics/hooks/useSetDefaultEventParametersMutation.mdx create mode 100644 docs/react/analytics/hooks/useSetUserIdMutation.mdx create mode 100644 docs/react/analytics/hooks/useSetUserPropertiesMutation.mdx create mode 100644 docs/react/analytics/index.mdx diff --git a/docs.json b/docs.json index 09eba7c4..03fd4181 100644 --- a/docs.json +++ b/docs.json @@ -78,6 +78,57 @@ } ] }, + { + "tab": "react", + "group": "Analytics", + "pages": [ + { + "title": "Getting Started", + "href": "/react/analytics" + }, + { + "group": "Hooks", + "pages": [ + { + "href": "/react/analytics/hooks/useGetGoogleAnalyticsClientIdQuery", + "title": "useGetGoogleAnalyticsClientIdQuery" + }, + { + "href": "/react/analytics/hooks/useIsSupportedQuery", + "title": "useIsSupportedQuery" + }, + { + "href": "/react/analytics/hooks/useLogEventMutation", + "title": "useLogEventMutation" + }, + { + "href": "/react/analytics/hooks/useSetAnalyticsCollectionEnabledMutation", + "title": "useSetAnalyticsCollectionEnabledMutation" + }, + { + "href": "/react/analytics/hooks/useSetConsentMutation", + "title": "useSetConsentMutation" + }, + { + "href": "/react/analytics/hooks/useSetCurrentScreenMutation", + "title": "useSetCurrentScreenMutation" + }, + { + "href": "/react/analytics/hooks/useSetDefaultEventParametersMutation", + "title": "useSetDefaultEventParametersMutation" + }, + { + "href": "/react/analytics/hooks/useSetUserIdMutation", + "title": "useSetUserIdMutation" + }, + { + "href": "/react/analytics/hooks/useSetUserPropertiesMutation", + "title": "useSetUserPropertiesMutation" + } + ] + } + ] + }, { "tab": "react", "group": "Authentication", diff --git a/docs/react/analytics/hooks/useGetGoogleAnalyticsClientIdQuery.mdx b/docs/react/analytics/hooks/useGetGoogleAnalyticsClientIdQuery.mdx new file mode 100644 index 00000000..92b637c1 --- /dev/null +++ b/docs/react/analytics/hooks/useGetGoogleAnalyticsClientIdQuery.mdx @@ -0,0 +1,30 @@ +--- +title: useGetGoogleAnalyticsClientIdQuery +--- + +Retrieve the Google Analytics client ID for a Firebase Analytics instance. + +## Usage + +```tsx +import { getAnalytics } from "firebase/analytics"; +import { + analyticsQueryKeys, + useGetGoogleAnalyticsClientIdQuery, +} from "@tanstack-query-firebase/react/analytics"; + +const analytics = getAnalytics(app); + +const { data: clientId, isLoading } = useGetGoogleAnalyticsClientIdQuery( + analytics, + { + queryKey: analyticsQueryKeys.googleAnalyticsClientId(analytics.app.name), + }, +); +``` + +## Notes + +- Wraps Firebase [`getGoogleAnalyticsClientId`](https://firebase.google.com/docs/reference/js/analytics.md#getgoogleanalyticsclientid). +- Requires a caller-provided `queryKey`. +- Disable the query until Analytics is initialized with `enabled: !!analytics` if needed. diff --git a/docs/react/analytics/hooks/useIsSupportedQuery.mdx b/docs/react/analytics/hooks/useIsSupportedQuery.mdx new file mode 100644 index 00000000..b4a99b5c --- /dev/null +++ b/docs/react/analytics/hooks/useIsSupportedQuery.mdx @@ -0,0 +1,29 @@ +--- +title: useIsSupportedQuery +--- + +Check whether Firebase Analytics is supported in the current environment. + +## Usage + +```tsx +import { + analyticsQueryKeys, + useIsSupportedQuery, +} from "@tanstack-query-firebase/react/analytics"; + +const { data: supported, isLoading } = useIsSupportedQuery({ + queryKey: analyticsQueryKeys.isSupported(), + staleTime: Number.POSITIVE_INFINITY, +}); + +if (supported) { + // Safe to call getAnalytics() +} +``` + +## Notes + +- Wraps Firebase [`isSupported`](https://firebase.google.com/docs/reference/js/analytics.md#issupported). +- Does not accept an `Analytics` instance. +- Requires a caller-provided `queryKey`. diff --git a/docs/react/analytics/hooks/useLogEventMutation.mdx b/docs/react/analytics/hooks/useLogEventMutation.mdx new file mode 100644 index 00000000..db705d09 --- /dev/null +++ b/docs/react/analytics/hooks/useLogEventMutation.mdx @@ -0,0 +1,46 @@ +--- +title: useLogEventMutation +--- + +Log a Google Analytics event via Firebase Analytics. + +## Usage + +```tsx +import { getAnalytics } from "firebase/analytics"; +import { useLogEventMutation } from "@tanstack-query-firebase/react/analytics"; + +const analytics = getAnalytics(app); +const { mutate: logAnalyticsEvent } = useLogEventMutation(analytics); + +logAnalyticsEvent({ + eventName: "login", + eventParams: { method: "email" }, +}); +``` + +## Screen tracking + +Prefer a `screen_view` event over the deprecated `useSetCurrentScreenMutation`: + +```tsx +logAnalyticsEvent({ + eventName: "screen_view", + eventParams: { + firebase_screen: "Home", + firebase_screen_class: "HomeScreen", + }, +}); +``` + +## Global events + +Apply an event to all Google Analytics properties on the page with `callOptions.global`: + +```tsx +logAnalyticsEvent({ + eventName: "purchase", + eventParams: { transaction_id: "T123", value: 9.99, currency: "USD" }, + callOptions: { global: true }, +}); +``` diff --git a/docs/react/analytics/hooks/useSetAnalyticsCollectionEnabledMutation.mdx b/docs/react/analytics/hooks/useSetAnalyticsCollectionEnabledMutation.mdx new file mode 100644 index 00000000..e69b8690 --- /dev/null +++ b/docs/react/analytics/hooks/useSetAnalyticsCollectionEnabledMutation.mdx @@ -0,0 +1,22 @@ +--- +title: useSetAnalyticsCollectionEnabledMutation +--- + +Enable or disable Google Analytics collection for an app on the current device. + +## Usage + +```tsx +import { getAnalytics } from "firebase/analytics"; +import { useSetAnalyticsCollectionEnabledMutation } from "@tanstack-query-firebase/react/analytics"; + +const analytics = getAnalytics(app); +const { mutate: setCollectionEnabled } = + useSetAnalyticsCollectionEnabledMutation(analytics); + +// Opt user out of analytics collection +setCollectionEnabled(false); + +// Re-enable after consent is granted +setCollectionEnabled(true); +``` diff --git a/docs/react/analytics/hooks/useSetConsentMutation.mdx b/docs/react/analytics/hooks/useSetConsentMutation.mdx new file mode 100644 index 00000000..1e6083db --- /dev/null +++ b/docs/react/analytics/hooks/useSetConsentMutation.mdx @@ -0,0 +1,23 @@ +--- +title: useSetConsentMutation +--- + +Set end-user consent state for Google Analytics across all gtag references on the page. + +## Usage + +```tsx +import { useSetConsentMutation } from "@tanstack-query-firebase/react/analytics"; + +const { mutate: setAnalyticsConsent } = useSetConsentMutation(); + +setAnalyticsConsent({ + analytics_storage: "granted", + ad_storage: "denied", +}); +``` + +## Notes + +- Wraps Firebase [`setConsent`](https://firebase.google.com/docs/reference/js/analytics.md#setconsent). +- This is a module-level API and does not require an `Analytics` instance. diff --git a/docs/react/analytics/hooks/useSetCurrentScreenMutation.mdx b/docs/react/analytics/hooks/useSetCurrentScreenMutation.mdx new file mode 100644 index 00000000..c6e42006 --- /dev/null +++ b/docs/react/analytics/hooks/useSetCurrentScreenMutation.mdx @@ -0,0 +1,34 @@ +--- +title: useSetCurrentScreenMutation +--- + +Set the current screen name via gtag config. + + +This hook wraps a deprecated Firebase API. Prefer [`useLogEventMutation`](/react/analytics/hooks/useLogEventMutation) with `eventName: "screen_view"` for new code. + + +## Usage + +```tsx +import { getAnalytics } from "firebase/analytics"; +import { useSetCurrentScreenMutation } from "@tanstack-query-firebase/react/analytics"; + +const analytics = getAnalytics(app); +const { mutate: setScreen } = useSetCurrentScreenMutation(analytics); + +setScreen({ screenName: "Home" }); +``` + +## Recommended alternative + +```tsx +import { useLogEventMutation } from "@tanstack-query-firebase/react/analytics"; + +const { mutate: logAnalyticsEvent } = useLogEventMutation(analytics); + +logAnalyticsEvent({ + eventName: "screen_view", + eventParams: { firebase_screen: "Home" }, +}); +``` diff --git a/docs/react/analytics/hooks/useSetDefaultEventParametersMutation.mdx b/docs/react/analytics/hooks/useSetDefaultEventParametersMutation.mdx new file mode 100644 index 00000000..8e92ddbc --- /dev/null +++ b/docs/react/analytics/hooks/useSetDefaultEventParametersMutation.mdx @@ -0,0 +1,22 @@ +--- +title: useSetDefaultEventParametersMutation +--- + +Set default event parameters applied to every Analytics event on the page. + +## Usage + +```tsx +import { useSetDefaultEventParametersMutation } from "@tanstack-query-firebase/react/analytics"; + +const { mutate: setDefaultParams } = useSetDefaultEventParametersMutation(); + +setDefaultParams({ session_id: "abc123", debug_mode: true }); +setDefaultParams({ campaign: "spring_sale" }); +``` + +## Notes + +- Wraps Firebase [`setDefaultEventParameters`](https://firebase.google.com/docs/reference/js/analytics.md#setdefaulteventparameters). +- This is a module-level API and does not require an `Analytics` instance. +- Parameters persist for subsequent automatic and manual events on the page. diff --git a/docs/react/analytics/hooks/useSetUserIdMutation.mdx b/docs/react/analytics/hooks/useSetUserIdMutation.mdx new file mode 100644 index 00000000..63d69ca1 --- /dev/null +++ b/docs/react/analytics/hooks/useSetUserIdMutation.mdx @@ -0,0 +1,28 @@ +--- +title: useSetUserIdMutation +--- + +Set or clear the Google Analytics user ID for a Firebase Analytics instance. + +## Usage + +```tsx +import { getAnalytics } from "firebase/analytics"; +import { useSetUserIdMutation } from "@tanstack-query-firebase/react/analytics"; + +const analytics = getAnalytics(app); +const { mutate: setAnalyticsUserId } = useSetUserIdMutation(analytics); + +setAnalyticsUserId({ id: user.uid }); + +// Clear user ID on sign-out +setAnalyticsUserId({ id: null }); +``` + +## Global user ID + +Apply the user ID across all gtag properties on the page: + +```tsx +setAnalyticsUserId({ id: user.uid, callOptions: { global: true } }); +``` diff --git a/docs/react/analytics/hooks/useSetUserPropertiesMutation.mdx b/docs/react/analytics/hooks/useSetUserPropertiesMutation.mdx new file mode 100644 index 00000000..d4ae14b5 --- /dev/null +++ b/docs/react/analytics/hooks/useSetUserPropertiesMutation.mdx @@ -0,0 +1,31 @@ +--- +title: useSetUserPropertiesMutation +--- + +Set Google Analytics user properties for a Firebase Analytics instance. + +## Usage + +```tsx +import { getAnalytics } from "firebase/analytics"; +import { useSetUserPropertiesMutation } from "@tanstack-query-firebase/react/analytics"; + +const analytics = getAnalytics(app); +const { mutate: setAnalyticsUserProperties } = + useSetUserPropertiesMutation(analytics); + +setAnalyticsUserProperties({ + properties: { plan: "premium", role: "admin" }, +}); +``` + +## Global properties + +Apply properties across all gtag properties on the page: + +```tsx +setAnalyticsUserProperties({ + properties: { plan: "free" }, + callOptions: { global: true }, +}); +``` diff --git a/docs/react/analytics/index.mdx b/docs/react/analytics/index.mdx new file mode 100644 index 00000000..ec65de4f --- /dev/null +++ b/docs/react/analytics/index.mdx @@ -0,0 +1,108 @@ +--- +title: Firebase Analytics +--- + +Firebase Analytics collects usage and behavior data for your app. These hooks wrap the Firebase Analytics web SDK with TanStack Query for reads and writes. + +Before using these hooks, ensure Analytics is enabled in your Firebase project and that the current environment supports it (Analytics is not available in all environments, such as some server-side contexts). + +## Setup + +```ts +import { initializeApp } from "firebase/app"; +import { getAnalytics } from "firebase/analytics"; + +// Initialize your Firebase app +initializeApp({ ... }); + +// Get the Analytics instance (client-side only) +const analytics = getAnalytics(app); +``` + +Use [`useIsSupportedQuery`](/react/analytics/hooks/useIsSupportedQuery) to check whether Analytics is supported before calling `getAnalytics()`. + +## Importing + +Hooks are exported from the `analytics` namespace: + +```ts +import { + analyticsQueryKeys, + useLogEventMutation, +} from "@tanstack-query-firebase/react/analytics"; +``` + +Most hooks accept an `Analytics` instance from `getAnalytics()` or `initializeAnalytics()`. Module-level Firebase APIs (`setConsent`, `setDefaultEventParameters`) are exposed as mutations that do not require an `Analytics` instance. + +## Query keys + +Analytics query hooks require a caller-provided `queryKey` (same as Firestore). Use `analyticsQueryKeys` for consistent keys: + +```tsx +import { analyticsQueryKeys, useIsSupportedQuery } from "@tanstack-query-firebase/react/analytics"; + +const { data: supported } = useIsSupportedQuery({ + queryKey: analyticsQueryKeys.isSupported(), + staleTime: Number.POSITIVE_INFINITY, +}); +``` + +## Logging events + +Use `useLogEventMutation` to send Google Analytics events. For screen tracking, prefer a `screen_view` event over the deprecated `useSetCurrentScreenMutation`: + +```tsx +import { getAnalytics } from "firebase/analytics"; +import { useLogEventMutation } from "@tanstack-query-firebase/react/analytics"; + +const analytics = getAnalytics(app); +const { mutate: logAnalyticsEvent } = useLogEventMutation(analytics); + +logAnalyticsEvent({ + eventName: "screen_view", + eventParams: { + firebase_screen: "Home", + firebase_screen_class: "HomeScreen", + }, +}); +``` + +## User identity and properties + +Set the Analytics user ID and custom user properties with mutation hooks. Clear the user ID on sign-out by passing `null`: + +```tsx +import { useSetUserIdMutation, useSetUserPropertiesMutation } from "@tanstack-query-firebase/react/analytics"; + +const { mutate: setAnalyticsUserId } = useSetUserIdMutation(analytics); +const { mutate: setAnalyticsUserProperties } = useSetUserPropertiesMutation(analytics); + +setAnalyticsUserId({ id: user.uid }); +setAnalyticsUserProperties({ properties: { plan: "premium" } }); + +// On sign-out +setAnalyticsUserId({ id: null }); +``` + +## Consent and collection + +- [`useSetConsentMutation`](/react/analytics/hooks/useSetConsentMutation) — set consent state across all gtag references on the page +- [`useSetAnalyticsCollectionEnabledMutation`](/react/analytics/hooks/useSetAnalyticsCollectionEnabledMutation) — enable or disable collection for the current device +- [`useSetDefaultEventParametersMutation`](/react/analytics/hooks/useSetDefaultEventParametersMutation) — attach default parameters to every event on the page + +## Available hooks + +### Queries + +- [`useGetGoogleAnalyticsClientIdQuery`](/react/analytics/hooks/useGetGoogleAnalyticsClientIdQuery) — retrieve the Google Analytics client ID +- [`useIsSupportedQuery`](/react/analytics/hooks/useIsSupportedQuery) — check whether Analytics is supported in the current environment + +### Mutations + +- [`useLogEventMutation`](/react/analytics/hooks/useLogEventMutation) — log a Google Analytics event +- [`useSetAnalyticsCollectionEnabledMutation`](/react/analytics/hooks/useSetAnalyticsCollectionEnabledMutation) — enable or disable analytics collection +- [`useSetConsentMutation`](/react/analytics/hooks/useSetConsentMutation) — set end-user consent settings +- [`useSetCurrentScreenMutation`](/react/analytics/hooks/useSetCurrentScreenMutation) — set the current screen name (deprecated) +- [`useSetDefaultEventParametersMutation`](/react/analytics/hooks/useSetDefaultEventParametersMutation) — set default event parameters +- [`useSetUserIdMutation`](/react/analytics/hooks/useSetUserIdMutation) — set or clear the Analytics user ID +- [`useSetUserPropertiesMutation`](/react/analytics/hooks/useSetUserPropertiesMutation) — set Analytics user properties diff --git a/docs/react/index.mdx b/docs/react/index.mdx index ce906320..e5fc1f61 100644 --- a/docs/react/index.mdx +++ b/docs/react/index.mdx @@ -89,6 +89,8 @@ function MyApplication() { } ``` -TanStack Query Firebase provides hooks for Firebase services including Authentication, Firestore, Realtime Database, and Data Connect, supporting both mutations and queries. +TanStack Query Firebase provides hooks for Firebase services including Analytics, Authentication, Firestore, Realtime Database, and Data Connect, supporting both mutations and queries. + +See the [Analytics documentation](/react/analytics) for event logging and user property hooks via `@tanstack-query-firebase/react/analytics`. See the [Realtime Database documentation](/react/database) for listener and write hooks via `@tanstack-query-firebase/react/database`. From c874a99277433b5a3d9a5612bddf2d610dc29106 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Wed, 27 May 2026 09:08:50 +0100 Subject: [PATCH 3/3] style(react): apply Biome formatting to analytics module --- packages/react/src/analytics/types.ts | 5 ++++- .../useGetGoogleAnalyticsClientIdQuery.test.tsx | 2 +- packages/react/src/analytics/useIsSupportedQuery.ts | 4 +++- .../src/analytics/useLogEventMutation.test.tsx | 13 +++++++++---- ...seSetAnalyticsCollectionEnabledMutation.test.tsx | 2 +- .../react/src/analytics/useSetConsentMutation.ts | 5 +---- .../analytics/useSetCurrentScreenMutation.test.tsx | 2 +- .../useSetDefaultEventParametersMutation.test.tsx | 3 +-- .../src/analytics/useSetUserIdMutation.test.tsx | 2 +- .../analytics/useSetUserPropertiesMutation.test.tsx | 2 +- .../src/analytics/useSetUserPropertiesMutation.ts | 6 +++++- 11 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/react/src/analytics/types.ts b/packages/react/src/analytics/types.ts index 44ba57c3..de75464f 100644 --- a/packages/react/src/analytics/types.ts +++ b/packages/react/src/analytics/types.ts @@ -1,4 +1,7 @@ -import type { UseMutationOptions, UseQueryOptions } from "@tanstack/react-query"; +import type { + UseMutationOptions, + UseQueryOptions, +} from "@tanstack/react-query"; import type { AnalyticsCallOptions, ConsentSettings, diff --git a/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.test.tsx b/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.test.tsx index 25621bd1..50f0c51a 100644 --- a/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.test.tsx +++ b/packages/react/src/analytics/useGetGoogleAnalyticsClientIdQuery.test.tsx @@ -1,7 +1,7 @@ import { renderHook, waitFor } from "@testing-library/react"; import type { Analytics } from "firebase/analytics"; -import type { FirebaseApp } from "firebase/app"; import { getGoogleAnalyticsClientId } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { queryClient, wrapper } from "../../utils"; import { analyticsQueryKeys } from "./queryKeys"; diff --git a/packages/react/src/analytics/useIsSupportedQuery.ts b/packages/react/src/analytics/useIsSupportedQuery.ts index dd7d189e..3e49db5d 100644 --- a/packages/react/src/analytics/useIsSupportedQuery.ts +++ b/packages/react/src/analytics/useIsSupportedQuery.ts @@ -29,7 +29,9 @@ import type { AnalyticsUseQueryOptions } from "./types"; * staleTime: Number.POSITIVE_INFINITY, * }); */ -export function useIsSupportedQuery(options: AnalyticsUseQueryOptions) { +export function useIsSupportedQuery( + options: AnalyticsUseQueryOptions, +) { return useQuery({ ...options, queryFn: () => isSupported(), diff --git a/packages/react/src/analytics/useLogEventMutation.test.tsx b/packages/react/src/analytics/useLogEventMutation.test.tsx index 8c48586d..ea9d709c 100644 --- a/packages/react/src/analytics/useLogEventMutation.test.tsx +++ b/packages/react/src/analytics/useLogEventMutation.test.tsx @@ -1,7 +1,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import type { Analytics } from "firebase/analytics"; -import type { FirebaseApp } from "firebase/app"; import { logEvent } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { queryClient, wrapper } from "../../utils"; import { useLogEventMutation } from "./useLogEventMutation"; @@ -34,9 +34,14 @@ describe("useLogEventMutation", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(logEvent).toHaveBeenCalledWith(mockAnalytics, "login", { - method: "email", - }, undefined); + expect(logEvent).toHaveBeenCalledWith( + mockAnalytics, + "login", + { + method: "email", + }, + undefined, + ); }); test("passes analytics call options", async () => { diff --git a/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.test.tsx b/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.test.tsx index 7d31e49c..2e48e3b2 100644 --- a/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.test.tsx +++ b/packages/react/src/analytics/useSetAnalyticsCollectionEnabledMutation.test.tsx @@ -1,7 +1,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import type { Analytics } from "firebase/analytics"; -import type { FirebaseApp } from "firebase/app"; import { setAnalyticsCollectionEnabled } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { queryClient, wrapper } from "../../utils"; import { useSetAnalyticsCollectionEnabledMutation } from "./useSetAnalyticsCollectionEnabledMutation"; diff --git a/packages/react/src/analytics/useSetConsentMutation.ts b/packages/react/src/analytics/useSetConsentMutation.ts index 2d583577..54468949 100644 --- a/packages/react/src/analytics/useSetConsentMutation.ts +++ b/packages/react/src/analytics/useSetConsentMutation.ts @@ -1,9 +1,6 @@ import { useMutation } from "@tanstack/react-query"; import { setConsent } from "firebase/analytics"; -import type { - AnalyticsUseMutationOptions, - SetConsentVariables, -} from "./types"; +import type { AnalyticsUseMutationOptions, SetConsentVariables } from "./types"; /** * Hook to set end-user consent state for Google Analytics across all gtag references. diff --git a/packages/react/src/analytics/useSetCurrentScreenMutation.test.tsx b/packages/react/src/analytics/useSetCurrentScreenMutation.test.tsx index 1cb06fa6..5cd0f9db 100644 --- a/packages/react/src/analytics/useSetCurrentScreenMutation.test.tsx +++ b/packages/react/src/analytics/useSetCurrentScreenMutation.test.tsx @@ -1,7 +1,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import type { Analytics } from "firebase/analytics"; -import type { FirebaseApp } from "firebase/app"; import { setCurrentScreen } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { queryClient, wrapper } from "../../utils"; import { useSetCurrentScreenMutation } from "./useSetCurrentScreenMutation"; diff --git a/packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx b/packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx index adbfc486..3c2dbf23 100644 --- a/packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx +++ b/packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx @@ -55,8 +55,7 @@ describe("useSetDefaultEventParametersMutation", () => { const onSuccessMock = vi.fn(); const { result } = renderHook( - () => - useSetDefaultEventParametersMutation({ onSuccess: onSuccessMock }), + () => useSetDefaultEventParametersMutation({ onSuccess: onSuccessMock }), { wrapper }, ); diff --git a/packages/react/src/analytics/useSetUserIdMutation.test.tsx b/packages/react/src/analytics/useSetUserIdMutation.test.tsx index 2ca03507..d94037b3 100644 --- a/packages/react/src/analytics/useSetUserIdMutation.test.tsx +++ b/packages/react/src/analytics/useSetUserIdMutation.test.tsx @@ -1,7 +1,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import type { Analytics } from "firebase/analytics"; -import type { FirebaseApp } from "firebase/app"; import { setUserId } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { queryClient, wrapper } from "../../utils"; import { useSetUserIdMutation } from "./useSetUserIdMutation"; diff --git a/packages/react/src/analytics/useSetUserPropertiesMutation.test.tsx b/packages/react/src/analytics/useSetUserPropertiesMutation.test.tsx index 9d293937..17020051 100644 --- a/packages/react/src/analytics/useSetUserPropertiesMutation.test.tsx +++ b/packages/react/src/analytics/useSetUserPropertiesMutation.test.tsx @@ -1,7 +1,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import type { Analytics } from "firebase/analytics"; -import type { FirebaseApp } from "firebase/app"; import { setUserProperties } from "firebase/analytics"; +import type { FirebaseApp } from "firebase/app"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { queryClient, wrapper } from "../../utils"; import { useSetUserPropertiesMutation } from "./useSetUserPropertiesMutation"; diff --git a/packages/react/src/analytics/useSetUserPropertiesMutation.ts b/packages/react/src/analytics/useSetUserPropertiesMutation.ts index 4eb45cd8..3a9f0694 100644 --- a/packages/react/src/analytics/useSetUserPropertiesMutation.ts +++ b/packages/react/src/analytics/useSetUserPropertiesMutation.ts @@ -35,7 +35,11 @@ import type { */ export function useSetUserPropertiesMutation( analytics: Analytics, - options?: AnalyticsUseMutationOptions, + options?: AnalyticsUseMutationOptions< + void, + Error, + SetUserPropertiesVariables + >, ) { return useMutation({ ...options,