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`.
diff --git a/packages/react/package.json b/packages/react/package.json
index b97b925e..db0b0daf 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..de75464f
--- /dev/null
+++ b/packages/react/src/analytics/types.ts
@@ -0,0 +1,80 @@
+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..50f0c51a
--- /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 { 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";
+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..3e49db5d
--- /dev/null
+++ b/packages/react/src/analytics/useIsSupportedQuery.ts
@@ -0,0 +1,39 @@
+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..ea9d709c
--- /dev/null
+++ b/packages/react/src/analytics/useLogEventMutation.test.tsx
@@ -0,0 +1,105 @@
+import { act, renderHook, waitFor } from "@testing-library/react";
+import type { Analytics } from "firebase/analytics";
+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";
+
+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..2e48e3b2
--- /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 { 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";
+
+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..54468949
--- /dev/null
+++ b/packages/react/src/analytics/useSetConsentMutation.ts
@@ -0,0 +1,43 @@
+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..5cd0f9db
--- /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 { 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";
+
+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..3c2dbf23
--- /dev/null
+++ b/packages/react/src/analytics/useSetDefaultEventParametersMutation.test.tsx
@@ -0,0 +1,70 @@
+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..d94037b3
--- /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 { 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";
+
+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..17020051
--- /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 { 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";
+
+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..3a9f0694
--- /dev/null
+++ b/packages/react/src/analytics/useSetUserPropertiesMutation.ts
@@ -0,0 +1,50 @@
+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<
+ void,
+ Error,
+ SetUserPropertiesVariables
+ >,
+) {
+ 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 6a55ddd7..d6214b3b 100644
--- a/packages/react/tsup.config.ts
+++ b/packages/react/tsup.config.ts
@@ -1,7 +1,13 @@
import * as fs from "node:fs/promises";
import { defineConfig } from "tsup";
-const supportedPackages = ["data-connect", "firestore", "auth", "database"];
+const supportedPackages = [
+ "analytics",
+ "data-connect",
+ "firestore",
+ "auth",
+ "database",
+];
export default defineConfig({
entry: [`src/(${supportedPackages.join("|")})/index.ts`, "src/index.ts"],
format: ["esm"],