Track user behavior in FlutterFlow by creating a Custom Action that logs events to a Firestore 'analytics_events' collection. Wire the action to page load (for screen views) and button taps (for interactions). Use a fire-and-forget async pattern to avoid blocking the UI. For richer analytics, add session ID, device info, and event properties to each event document.
Building a Custom Analytics Pipeline with FlutterFlow and Firestore
Understanding how users interact with your app is essential for making data-driven decisions about features, navigation, and UX. While third-party analytics tools like Google Analytics or Mixpanel exist, building a lightweight custom pipeline in Firestore gives you full ownership of the data and integrates naturally with the rest of your FlutterFlow backend. The pattern is simple: create a Custom Action that writes a structured event document to Firestore, then wire that action to page load events (for screen views), button taps, form submissions, and any other interactions you care about. This guide covers the full pipeline from Firestore schema design to Action Flow wiring.
Prerequisites
- FlutterFlow project with Firebase and Firestore connected
- Firebase Authentication set up so events can be linked to a user UID
- Basic familiarity with FlutterFlow Custom Actions and Action Flows
- Firestore analytics_events collection created (or let the Custom Action create it automatically)
Step-by-step guide
Design the Firestore analytics_events collection
Design the Firestore analytics_events collection
Open the Firebase Console and navigate to your project's Firestore Database. Create a new collection named 'analytics_events'. Each document in this collection represents one user action and should contain: event (String — the event name like 'screen_view' or 'button_tap'), screen (String — current page name), uid (String — Firebase Auth user ID), sessionId (String — random ID per app session), timestamp (Timestamp — Firestore server timestamp), properties (Map — flexible key-value data specific to the event). You do not need to create the collection manually if it does not exist — Firestore creates collections automatically on the first write. However, setting up the collection now lets you configure Firestore Security Rules to restrict who can write and read events.
Expected result: The analytics_events collection schema is defined and Firestore Security Rules allow authenticated users to write (but not read) their own events.
Create the logEvent Custom Action
Create the logEvent Custom Action
Open the Custom Code panel (</> in the left sidebar). In the Custom Actions tab, tap '+' and name the action 'logEvent'. Add these parameters: eventName (String, required), screenName (String, required), properties (JSON, optional). The action should write a document to the analytics_events collection using a fire-and-forget pattern — call the Firestore write but do NOT await it inside the UI-blocking action flow. Instead, use unawaited() from the dart:async library to start the Firestore write without blocking. This ensures your UI remains responsive even if the Firestore write is slow. Include the current user's UID and a session ID stored in app state.
1import 'dart:async';2import 'package:firebase_auth/firebase_auth.dart';3import 'package:cloud_firestore/cloud_firestore.dart';45Future logEvent(6 String eventName,7 String screenName,8 dynamic properties,9) async {10 final user = FirebaseAuth.instance.currentUser;11 if (user == null) {12 debugPrint('logEvent: skipped — no authenticated user');13 return;14 }1516 // Fire-and-forget: do not await the Firestore write17 unawaited(18 FirebaseFirestore.instance19 .collection('analytics_events')20 .add({21 'event': eventName,22 'screen': screenName,23 'uid': user.uid,24 'timestamp': FieldValue.serverTimestamp(),25 'properties': properties ?? {},26 }).catchError((e) {27 debugPrint('logEvent write error: $e');28 }),29 );30}Expected result: The logEvent Custom Action compiles successfully with three parameters and uses unawaited for the Firestore write.
Add a session ID to App State
Add a session ID to App State
A session ID groups all events from a single app session together, making it easy to analyze complete user journeys. In FlutterFlow, go to the left sidebar → App State (the database-like icon). Add a new App State variable named 'sessionId' of type String with no default value. Next, on your app's initial page (or a splash screen), add an 'On Page Load' action: 'Update App State' → set sessionId to a Custom Function that generates a random ID. Create a Custom Function named 'generateSessionId' that returns a UUID-style random string. This ID persists for the app session and resets each time the app is launched fresh.
1// Custom Function: generateSessionId2// Return type: String3// No parameters4String generateSessionId() {5 final now = DateTime.now().millisecondsSinceEpoch.toString();6 final rand = (1000000 + (9000000 * (DateTime.now().microsecond / 1000000))).toInt().toString();7 return 'sess_${now}_$rand';8}Expected result: App State has a sessionId variable, and the initial page's On Page Load action sets it to a fresh random value each session.
Wire logEvent to page load for screen view tracking
Wire logEvent to page load for screen view tracking
Navigate to a page you want to track — for example, your Home page. In the page properties panel, find the 'On Page Load' action trigger and open the Action Flow editor. Add a new action step: 'Custom Action' → 'logEvent'. Set eventName to the string 'screen_view', set screenName to the page name (e.g., 'Home'), and leave properties empty or pass a JSON object with additional context. Repeat this for each important page in your app. FlutterFlow's On Page Load trigger fires every time the user navigates to that page, giving you a complete picture of navigation patterns.
Expected result: Each tracked page has an On Page Load action that calls logEvent with screen_view, and new documents appear in the analytics_events Firestore collection when you navigate in Test Mode.
Wire logEvent to button taps and key interactions
Wire logEvent to button taps and key interactions
For button tap tracking, open the Action Flow for any button you want to track. At the beginning of the Action Flow (before the main action), add a 'Custom Action' → 'logEvent' step. Set eventName to a descriptive name like 'button_tap', screenName to the current page name, and properties to a JSON object with the button's identity: {"button": "purchase_cta", "plan": selectedPlanVariable}. Place the logEvent call at the start of the flow so it fires even if a later action fails. Track form submissions, purchases, social shares, and any other high-value interactions. Avoid tracking every single widget interaction — focus on actions that represent meaningful user intent.
Expected result: Key buttons in the app call logEvent at the start of their Action Flows, and event documents with the correct button names appear in Firestore when triggered in Test Mode.
Query and review your event data in Firebase Console
Query and review your event data in Firebase Console
Open the Firebase Console → Firestore → analytics_events collection. Use the filter panel to narrow events by uid (your test user's UID), event type, or date range. Review the document structure to confirm all fields are populated correctly. For session analysis, filter by sessionId to see the complete sequence of events for one session. To query programmatically (for a future analytics dashboard in your app), create a Firestore query in FlutterFlow's Backend Query panel on a Repeating Group: collection 'analytics_events', filter by uid = currentUser.uid, order by timestamp descending, limit 50. This shows the most recent events for the logged-in user.
Expected result: You can view and filter event documents in the Firebase Console and confirm that screen views, button taps, and other events are logged with correct field values.
Complete working example
1// ─── Custom Action: logEvent ─────────────────────────────────────────────────2// Fire-and-forget event logger to Firestore analytics_events collection.3// Parameters:4// eventName (String) — e.g. 'screen_view', 'button_tap', 'purchase'5// screenName (String) — current page name6// properties (JSON) — optional extra data, e.g. {"productId": "abc"}7//8// Wire to: On Page Load (screen views), button Action Flows (interactions)910import 'dart:async';11import 'package:firebase_auth/firebase_auth.dart';12import 'package:cloud_firestore/cloud_firestore.dart';1314Future logEvent(15 String eventName,16 String screenName,17 dynamic properties,18) async {19 final user = FirebaseAuth.instance.currentUser;20 if (user == null) {21 debugPrint('logEvent: no authenticated user — skipped.');22 return;23 }2425 final Map<String, dynamic> eventData = {26 'event': eventName,27 'screen': screenName,28 'uid': user.uid,29 'timestamp': FieldValue.serverTimestamp(),30 'properties': (properties is Map) ? Map<String, dynamic>.from(properties) : {},31 };3233 // Fire-and-forget: write to Firestore without blocking the UI34 unawaited(35 FirebaseFirestore.instance36 .collection('analytics_events')37 .add(eventData)38 .catchError((Object e) {39 debugPrint('logEvent write error: $e');40 }),41 );42 debugPrint('logEvent: queued event "$eventName" on screen "$screenName"');43}4445// ─── Custom Function: generateSessionId ──────────────────────────────────────46// Returns a unique session ID string. Call on app launch, store in App State.47// Return type: String, no parameters.4849String generateSessionId() {50 final ts = DateTime.now().millisecondsSinceEpoch;51 final micro = DateTime.now().microsecond;52 return 'sess_${ts}_$micro';53}5455// ─── Firestore Security Rules (add to your rules) ────────────────────────────56// rules_version = '2';57// service cloud.firestore {58// match /databases/{database}/documents {59// match /analytics_events/{event} {60// allow create: if request.auth != null61// && request.resource.data.uid == request.auth.uid;62// allow read: if false; // No client-side reads — query via admin SDK63// }64// }65// }Common mistakes
Why it's a problem: Logging events synchronously (with await) in the action flow, blocking the UI
How to avoid: Use unawaited() from dart:async to fire the Firestore write without waiting for a response. Attach a catchError handler to the unawaited future to log any write failures silently. The event is still written — the action just does not block on it.
Why it's a problem: Logging every widget interaction instead of only meaningful user actions
How to avoid: Log only high-intent actions: screen views, purchase events, form submissions, key feature usage, and explicit user choices. Scroll depth and hover events are better suited to a dedicated analytics SDK than a Firestore pipeline.
Why it's a problem: Not adding a sessionId to event documents
How to avoid: Generate a session ID on app launch using a Custom Function, store it in App State, and include it in every logEvent call via an App State parameter or by reading it directly inside the Custom Action.
Why it's a problem: Storing raw PII (names, emails, phone numbers) in event properties
How to avoid: Store only the user's UID (already a pseudonymous identifier) in event documents. Never store names, email addresses, or phone numbers. If you need to join events with user details, do it server-side by joining on UID.
Best practices
- Use a consistent event naming taxonomy: verb_noun format (e.g., 'screen_view', 'button_tap', 'form_submit', 'purchase_complete') makes queries and grouping much cleaner.
- Store properties as a nested Map rather than flat fields — this lets you add new event-specific data without changing the top-level document schema.
- Set Firestore Security Rules to allow only create (not read) on analytics_events for authenticated users — client apps should write events but never query them directly.
- Add a TTL (Time To Live) policy to your analytics_events collection using Firestore's document TTL feature to automatically delete events older than 90 days, controlling storage costs.
- Generate a new session ID each time the app is launched from scratch, but keep the same ID across page navigations within a session — this allows funnel analysis.
- Test your tracking by opening the Firebase Console and watching the analytics_events collection in real time as you navigate the app in Test Mode.
- For GDPR compliance, include a user consent check before logging any events — only call logEvent if the user has accepted your privacy policy.
- Build a simple admin dashboard page in FlutterFlow that queries the analytics_events collection and displays counts by event type in a ListView — this gives you a live view of your most-used features.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a FlutterFlow app with Firebase and want to track user behavior by logging events to a Firestore 'analytics_events' collection. Please write a FlutterFlow Custom Action in Dart called 'logEvent' that takes parameters: eventName (String), screenName (String), and properties (JSON/dynamic). It should use unawaited() to fire-and-forget the Firestore write so it does not block the UI. It should get the current user's UID from FirebaseAuth.instance.currentUser and include a server timestamp. Include error handling that logs failures with debugPrint.
Write a FlutterFlow Custom Action called 'logEvent' for a behavior analytics pipeline. Parameters: eventName (String), screenName (String), properties (JSON). It must use unawaited() for the Firestore write to avoid blocking the UI. Include: auth check (skip if no user), fire-and-forget write to 'analytics_events' collection with uid + FieldValue.serverTimestamp(), catchError that debugPrints failures. Write only the function body for the FlutterFlow Custom Code editor.
Frequently asked questions
How much will it cost to store analytics events in Firestore?
Firestore charges per document write ($0.18 per 100,000 writes on the Blaze plan). If your app has 1,000 active users each triggering 20 events per session with 2 sessions per day, that is 40,000 writes/day, which costs roughly $0.07/day or about $2/month. Set up Firestore document TTL to auto-delete old events and keep the collection size manageable.
Should I use Google Analytics or a custom Firestore pipeline for tracking?
Both have trade-offs. Google Analytics is free, has powerful dashboards, and requires no database writes. However, you have no control over the data and cannot query raw events from your app. A custom Firestore pipeline gives you full ownership, lets you build in-app analytics dashboards, and integrates with your existing backend queries. For most FlutterFlow apps, starting with a simple Firestore pipeline is faster and more flexible.
Can I log events for unauthenticated users?
Yes, but with caveats. You can use an anonymous Firebase Auth session (FirebaseAuth.instance.signInAnonymously()) to get a UID for unauthenticated users. This lets you track behavior before login and link the anonymous session to the real user after authentication using FirebaseAuth.instance.currentUser.linkWithCredential(). Add this anonymous sign-in to your app's initial page load action.
How do I query analytics events from within my FlutterFlow app?
Add a Backend Query on a Repeating Group: collection 'analytics_events', add a filter for uid equals currentUser.uid, order by timestamp descending, and set a limit (e.g., 100). This shows the logged-in user's recent events. For admin dashboards that query all users' events, use a separate admin-only page with a service-role-level Firestore query, or export data to BigQuery for analysis.
Is there a risk that analytics logging could slow down my app?
Not with the unawaited() pattern. The logEvent Custom Action starts the Firestore write and immediately returns — the UI proceeds without waiting. The only scenario where analytics could impact performance is if you call logEvent hundreds of times per second (e.g., on every scroll pixel), which you should avoid. For normal event logging (screen views, button taps), the performance impact is negligible.
What is the recommended Firestore Security Rules configuration for analytics_events?
Allow authenticated users to create documents where the uid field matches their own auth UID. Deny all read, update, and delete access from the client. This prevents users from reading other users' analytics data or tampering with event records. Admin reads should happen via a server-side Cloud Function or BigQuery export, not directly from the client.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation