/app/App_Resources/Android folder
/app/App_Resources/iOS folder
package.json
"nativescript-angular": "1.2.0", "nativescript-camera": "^0.0.8", "nativescript-iqkeyboardmanager": "^1.0.1", "nativescript-plugin-firebase": "^3.8.4", "nativescript-theme-core": "^1.0.2",
platforms
tns run ios
tns run android.
tns livesync ios --watch
tns livesync android --watch
app/main.ts
// this import should be first in order to load some required settings (like globals and reflect-metadata) import { platformNativeScriptDynamic } from "nativescript-angular/platform"; import { AppModule } from "./app.module"; import { BackendService } from "./services/backend.service"; import firebase = require("nativescript-plugin-firebase"); firebase.init({ //persist should be set to false as otherwise numbers aren't returned during livesync persist: false, storageBucket: 'gs://giftler-f48c4.appspot.com', onAuthStateChanged: (data: any) => { console.log(JSON.stringify(data)) if (data.loggedIn) { BackendService.token = data.user.uid; } else { BackendService.token = ""; } } }).then( function (instance) { console.log("firebase.init done"); }, function (error) { console.log("firebase.init error: " + error); } ); platformNativeScriptDynamic().bootstrapModule(AppModule);
app/login/login.component.ts
user@nativescript.org
onAuthStateChanged
onAuthStateChanged: (data: any) => { console.log(JSON.stringify(data)) if (data.loggedIn) { BackendService.token = data.user.uid; } else { BackendService.token = ""; } }
loggedIn
token
app/services/backend.service.ts
app/auth-guard.service.ts
export class AuthGuard implements CanActivate { constructor(private router: Router) { } canActivate() { if (BackendService.isLoggedIn()) { return true; } else { this.router.navigate(["/login"]); return false; } }
const listRoutes: Routes = [ { path: "", component: ListComponent, canActivate: [AuthGuard] }, ];
app/list/list.html
app/list/list.component.ts
public gifts$: Observable;
ngOnInit(){ this.gifts$ = this.firebaseService.getMyWishList(); }
getMyWishList(): Observable { return new Observable((observer: any) => { let path = 'Gifts'; let onValueEvent = (snapshot: any) => { this.ngZone.run(() => { let results = this.handleSnapshot(snapshot.value); console.log(JSON.stringify(results)) observer.next(results); }); }; firebase.addValueEventListener(onValueEvent, `/${path}`); }).share(); }
handleSnapshot
handleSnapshot(data: any) { //empty array, then refill and filter this._allItems = []; if (data) { for (let id in data) { let result = (Object).assign({id: id}, data[id]); if(BackendService.token === result.UID){ this._allItems.push(result); } } this.publishUpdates(); } return this._allItems; }
publishUpdates() { // here, we sort must emit a *new* value (immutability!) this._allItems.sort(function(a, b){ if(a.date < b.date) return -1; if(a.date > b.date) return 1; return 0; }) this.items.next([...this._allItems]); }
<Label class="gold card" textWrap="true" [text]="message$ | async"></Label>
message$
ngOnInit(){ this.message$ = this.firebaseService.getMyMessage(); }
(app/services/firebase.service.ts
getMyMessage(): Observable{ return new Observable((observer:any) => { firebase.getRemoteConfig({ developerMode: false, cacheExpirationSeconds: 300, properties: [{ key: "message", default: "Happy Holidays!" }] }).then( function (result) { console.log("Fetched at " + result.lastFetch + (result.throttled ? " (throttled)" : "")); for (let entry in result.properties) { observer.next(result.properties[entry]); } } ); }).share(); }
app/list-detail/list-detail.component.ts
ngOnInit() { camera.requestPermissions(); ... }
takePhoto() { let options = { width: 300, height: 300, keepAspectRatio: true, saveToGallery: true }; camera.takePicture(options) .then(imageAsset => { imageSource.fromAsset(imageAsset).then(res => { this.image = res; //save the source image to a file, then send that file path to firebase this.saveToFile(this.image); }) }).catch(function (err) { console.log("Error -> " + err.message); }); }
saveToFile(res){ let imgsrc = res; this.imagePath = this.utilsService.documentsPath(`photo-${Date.now()}.png`); imgsrc.saveToFile(this.imagePath, enums.ImageFormat.png); }
/Gifts
editGift(id: string){ if(this.image){ //upload the file, then save all this.firebaseService.uploadFile(this.imagePath).then((uploadedFile: any) => { this.uploadedImageName = uploadedFile.name; //get downloadURL and store it as a full path; this.firebaseService.getDownloadUrl(this.uploadedImageName).then((downloadUrl: string) => { this.firebaseService.editGift(id,this.description,downloadUrl).then((result:any) => { alert(result) }, (error: any) => { alert(error); }); }) }, (error: any) => { alert('File upload error: ' + error); }); } else { //just edit the description this.firebaseService.editDescription(id,this.description).then((result:any) => { alert(result) }, (error: any) => { alert(error); }); } }
uploadFile(localPath: string, file?: any): Promise { let filename = this.utils.getFilename(localPath); let remotePath = `${filename}`; return firebase.uploadFile({ remoteFullPath: remotePath, localFullPath: localPath, onProgress: function(status) { console.log("Uploaded fraction: " + status.fractionCompleted); console.log("Percentage complete: " + status.percentageCompleted); } }); } getDownloadUrl(remoteFilePath: string): Promise { return firebase.getDownloadUrl({ remoteFullPath: remoteFilePath}) .then( function (url:string) { return url; }, function (errorMessage:any) { console.log(errorMessage); }); } editGift(id:string, description: string, imagepath: string){ this.publishUpdates(); return firebase.update("/Gifts/"+id+"",{ description: description, imagepath: imagepath}) .then( function (result:any) { return 'You have successfully edited this gift!'; }, function (errorMessage:any) { console.log(errorMessage); }); }
The Santa Tracker app for Android is a Google holiday tradition. Every year, millions of people around the world use the app to play games with elves and reindeer and, of course, track Santa, as he flies around the world on December 24th. While the app is live for a few months each year, about 90% of our usage occurs in the last two weeks of December. In order to turn around improvements to Santa Tracker quickly over this time, it's critical that we can monitor and adjust the Santa Tracker app remotely. This year, we decided to go all-in with Firebase as our monitoring solution. In this blog post, I'll talk about how we use a combination Analytics, Crash Reporting, and Remote Config to maintain a high level of quality, without ever having to republish the app.
As users navigate through the app we use Firebase Analytics events to record their behavior. Most of the mini-games in the app live in their own Activity classes, so we can use Firebase Analytics' automatic screen tracking feature to record these events without writing any code.
For events within games we use custom events to record important user actions. For example after the user finishes playing the "Penguin Swim" game, we record the event swimming_game_end with custom parameters score and num_stars. In the first week of December we noticed that 85% of users were getting zero stars when playing the Penguin Swim game. Clearly, the game is too hard, we were hoping that only 60-70% of users would get a score this low! We were able to correct this using Remote Config, which I'll talk about later.
swimming_game_end
score
num_stars
The other feature of Analytics that we put to use is user properties. At the start of each Santa Tracker session, we use user properties to record some information about the user's device. These properties are then attached to every analytics event. Since Santa Tracker is used all over the world, we get a lot of diversity in the devices people use. These user properties help us to make sense of our analytics data. Some examples are:
API_LEVEL
DEVICE_BRAND
DEVICE_BOARD
The combination of our custom events and user properties with Firebase Analytics' automatically tracked events enables us to get a good understanding of what our users are doing in the app by looking at the Firebase console.
Despite our best efforts, the Santa Tracker app is not perfect. With millions of users on hundreds of device types in dozens of countries we are constantly discovering new bugs in the wild. Firebase Crash Reporting lets us see all of the fatal errors in our app within a minute of their occurrence. Since Firebase Analytics events show up in Firebase Crash Reporting logs we can see the progression of events before the crash which was very helpful in diagnosing some issues.
For example there's an OutOfMemoryError crash which seems to happen during the "Penguin Swim" game on some low-RAM devices. We did not see this error during our testing, but the Firebase Analytics data in Crash Reporting tells us that this occurs when playing the game repeatedly.
OutOfMemoryError
This integration is invaluable in helping us to reproduce issues that our normal QA setup does not find. We can get the exact device model and then use the analytics log to recreate the crash conditions.
Once we have analyzed the data from Analytics and Crash Reporting, we need to make changes in the app to improve the user experience. Due to the short active life span of this app there's no time to go through the full development lifecycle of the app to publish changes, and we don't get a second chance at Santa's big day!
Santa Tracker uses Firebase Remote Config to gate access to various features, and to provide remote fine-tuning for experiences in the mini game. For example, in the "Penguin Swim" game, there are two key variables we store in Remote Config:
SwimmingObstacleDensity
DisableSwimmingGame
As mentioned earlier, users were having a hard time getting a score higher than zero stars in the game. In order to make the game more fun, we changed SwimmingObstacleDensity from 1.5 to 1.1, which made it much easier for users to dodge obstacles. By making the game easier in this way, the percentage of users getting 0 stars went down from about 85% to 70%. This change took place instantly over the air, with no need to publish a new version of the app!
Right now the OutOfMemoryError in the swimming game happens for <1% of users. But if this issue became rampant, we could use the DisableSwimmingGame flag to immediately hide the game from affected users whilst we resolve the issue. By taking advantage of the fact that Analytics user properties can be referenced in Remote Config, we can even disable the game only for certain device types! For example, let's say the Penguin Swim stopped working on all KitKat devices (API level 19).
First, we add a condition based on user properties:
Next, we disable the game only for users who match the condition:
Now the game will only appear for users who will have a stable experience, which will lead to fewer crashes for our users and more positive app ratings for us.
Adding deep Firebase integration to Santa Tracker gives us the ability to monitor and fine-tune the app over time without releasing app updates. As developers, it's invaluable to have a clear picture of what our users are really doing and how we can improve the app. Throughout December we knew we could rely on Firebase to give Santa Tracker users a magical holiday experience.
If you've been working with Firebase on Android, you may have noticed that you don't normally have to write any lines of code to initialize a feature. You just grab the singleton object for that feature, and start using it right away. And, in the case of Firebase Crash Reporting, you don't even have to write any code at all for it to start capturing crashes! This question pops up from time to time, and I talked about it a bit at Google I/O 2016, but I'd also like to break it down in detail here.
The problem
Many SDKs need an Android Context to be able to do their work. This Context is the hook into the Android runtime that lets the SDK access app resources and assets, use system services, and register BroadcastReceivers. Many SDKs ask you to pass a Context into a static init method once, so they can hold and use that reference as long as the app process is alive. In order to get that Context at the time the app starts up, it's common for the developers of the SDK to ask you to pass that in a custom Application subclass like this:
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); SomeSdk.init(this); // init some SDK, MyApplication is the Context } }
And if you hadn't already registered a custom subclass in your app, you'd also have to add that to your manifest in the application tag's android:name attribute:
<application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:name="package.of.MyApplication" ... >
All this is fine, but Firebase SDKs make this a lot easier for its users!
The solution
There is a little trick that the Firebase SDKs for Android use to install a hook early in the process of an application launch cycle. It introduces a ContentProvider to implement both the timing and Context needed to initialize an SDK, but without requiring the app developer to write any code. A ContentProvider is a convenient choice for two reasons:
Let's investigate those two properties.
ContentProvider initializes early
When an Android app process is first started, there is well-defined order of operations:
When a ContentProvider is created, Android will call its onCreate method. This is where the Firebase SDK can get a hold of a Context, which it does by calling the getContext method. This Context is safe to hold on to indefinitely.
This is also a place that can be used to set up things that need to be active throughout the app's lifetime, such as ActivityLifecycleCallbacks (which are used by Firebase Analytics), or a UncaughtExceptionHandler (which is used by Firebase Crash Reporting). You might also initialize a dependency injection framework here.
ContentProviders participate in manifest merger
Manifest merge is a process that happens at build time when the Android build tools need to figure out the contents of the final manifest that defines your app. In your app's AndroidManifest.xml file, you declare all your application components, permissions, hardware requirements, and so on. But the final manifest that gets built into the APK contains all of those elements from all of the Android library projects that your app depends on.
It turns out that ContentProviders are merged into the final manifest as well. As a result, any Android library project can simply declare a ContentProvider in its own manifest, and that entry will end up in the app's final manifest. So, when you declare a dependency on Firebase Crash Reporting, the ContentProvider from its manifest is merged in your own app's manifest. This ensures that its onCreate is executed, without you having to write any code.
FirebaseInitProvider (surprise!) initializes your app
All apps using Firebase in some way will have a dependency on the firebase-common library. This library exposes FirebaseInitProvider, whose responsibility is to call FirebaseApp.initializeApp in order to initialize the default FirebaseApp instance using the configurations from the project's google-services.json file. (Those configurations are injected into the build as Android resources by the Google Services plugin.) However, If you're referencing multiple Firebase projects in one app, you'll have to write code to initialize other FirebaseApp instances, as discussed in an earlier blog post.
Some drawbacks with ContentProvider init
If you choose to use a ContentProvider to initialize your app or library, there's a couple things you need to keep in mind.
First, there can be only one ContentProvider on an Android device with a given "authority" string. So, if your library is used in more than one app on a device, you have to make sure that they get added with two different authority strings, or the second app will be rejected for installation. That string is defined for the ContentProvider in the manifest XML, which means it's effectively hard-coded. But there is a trick you can use with the Android build tools to make sure that each app build declares a different authority.
There is a feature of Android Gradle builds call manifest placeholders that lets you declare and insert a placeholder value that get inserted into manifest strings. The app's unique application ID is automatically available as a placeholder, so you can declare your ContentProvider like this:
<provider android:authorities="${applicationId}.yourcontentprovider" android:name=".YourContentProvider" android:exported="false" />
The other thing to know about about ContentProviders is that they are only run in the main process of an app. For a vast majority of apps, this isn't a problem, as there is only one process by default. But the moment you declare that one of the Android components in your app must run in another process, that process won't create any ContentProviders, which means your ContentProvider onCreate will never get invoked. In this case, the app will have to either avoid calling anything that requires the initialization, or safely initialize another way. Note that this behavior is different than a custom Application subclass, which does get invoked in every process for that app.
But why misuse ContentProvider like this?
Yes, it's true, this particular application of ContentProvider seems really weird, since it's not actually providing any content. And you have to provide implementations of all the other ContentProvider required methods by returning null. But, it turns out that this is the most reliable way to automatically initialize without requiring extra code. I think the convenience for developers using Firebase more than makes up for this strangeness of this use of a ContentProvider. Firebase is all about being easy to use, and there's nothing easier than no code at all!
Firebase provides a bunch of features to use together in your app, provided by a project that you create at the Firebase console. Normally, it's sufficient to have all your app's resources provided by a single project, but there are times when you want a single app to be able to access data from multiple projects. For example, you may need to access data from two different databases, and be able to authenticate users to access each one. I'll show you how that's done in this post.
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(getString(R.string.default_web_client_id)) .requestEmail() .build();
google-services.json
FirebaseDatabase database = FirebaseDatabase.getInstance();
FirebaseOptions options = new FirebaseOptions.Builder() .setApplicationId("1:530266078999:android:481c4ecf3253701e") // Required for Analytics. .setApiKey("AIzaSyBRxOyIj5dJkKgAVPXRLYFkdZwh2Xxq51k") // Required for Auth. .setDatabaseUrl("https://project-1765055333176374514.firebaseio.com/") // Required for RTDB. .build(); FirebaseApp.initializeApp(this /* Context */, options, "secondary");
FirebaseApp
FirebaseDatabase.getInstance()
// Retrieve my other app. FirebaseApp app = FirebaseApp.getInstance("secondary"); // Get the database for the other app. FirebaseDatabase secondaryDatabase = FirebaseDatabase.getInstance(app);
.requestIdToken(getString(R.string.default_web_client_id))
{ "client_id": "56865680640-e8mr503bun5eaevqctn4u807q4hpi44s.apps.googleusercontent.com", "client_type": 3 },
AuthCredential credential = GoogleAuthProvider.getCredential(account.getIdToken(), null); FirebaseAuth.getInstance().signInWithCredential(credential); FirebaseApp app = FirebaseApp.getInstance("secondary"); FirebaseAuth.getInstance(app).signInWithCredential(credential);
Default Auth UID: 0960868722032022577213DA4EA8B7A1683D92B405DD Secondary Auth UID: 7h6XOeSxmkNsSseFJ1jU31WZHDP2
firebaseAuth.getCurrentUser().getToken(false /* forceRefresh */) .addOnCompleteListener(new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { String token = task.getResult().getToken(); // Send this to the server. } });
FirebaseOptions options = new FirebaseOptions.Builder() .setServiceAccount(new FileInputStream("default-service-account.json")) .build(); FirebaseApp.initializeApp(options); FirebaseOptions secondaryOptions = new FirebaseOptions.Builder() .setServiceAccount(new FileInputStream("secondary-service-account.json")) .build(); FirebaseApp.initializeApp(secondaryOptions, "secondary");
// Verify the ID token using the default app. FirebaseAuth.getInstance().verifyIdToken(idToken) .addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(FirebaseToken decodedToken) { String uid = decodedToken.getUid(); System.out.println("User " + uid + " verified"); FirebaseApp app = FirebaseApp.getInstance("secondary"); String customToken = FirebaseAuth.getInstance(app).createCustomToken(uid); // TODO: Send the token back to the client! } });
FirebaseApp app = FirebaseApp.getInstance("secondary"); FirebaseAuth.getInstance(app).signInWithCustomToken(token);
Default Auth UID: 0960868722032022577213DA4EA8B7A1683D92B405DD Secondary Auth UID: 0960868722032022577213DA4EA8B7A1683D92B405DD
var config = { apiKey: "", authDomain: ".firebaseapp.com", databaseURL: "https://.firebaseio.com", storageBucket: ".appspot.com", messagingSenderId: "", }; var secondary = firebase.initializeApp(otherAppConfig, "secondary"); var secondaryDatabase = secondary.database();
// Alt: load from plist using |FIROptions(contentsOfFile:)| let options = FIROptions(googleAppID: googleAppID, bundleID: bundleID, GCMSenderID: GCMSenderID, APIKey: nil, clientID: nil, trackingID: nil, androidClientID: nil, databaseURL: databaseURL, storageBucket: nil, deepLinkURLScheme: nil) FIRApp.configure(withName: "secondary", options: fileopts) guard let secondary = FIRApp.init(named: "secondary") else { assert(false, "Could not retrieve secondary app") } let secondaryDatabase = FIRDatabase.database(app: secondary);
Over the course of this series, I've introduced the concept of Pirate Metrics, followed by individual posts discussing how to track (and improve) acquisition, activation and retention with Firebase and its' suite of products.
Every product owner dreams of seeing the work they create go viral. When your users love what you built so much that they want everyone around them to use it as well, it validates all the effort and hard work that went into it.
But here's the thing: while your users are typically more than happy to refer your application to their friends, family and colleagues, they are unlikely to be interested in putting in a lot of effort. The simple, easy way is to ensure you make it easy to simply share the URL for your application. However, you want to easily track how your referrals are doing, and you also want to smoothen the onboarding process for the new, incoming user.
The first product I'd like to talk about in this post is Dynamic Links. These links, which can be generated either on the Firebase console or programmatically, offer the benefit of redirecting users appropriately based on where they're opening them. That is, users on Android can be automatically sent to the Play Store while users on iOS can be sent to the App Store. If the user already has the app, they can be deeplinked to specific content inside it.
You can take Dynamic Links a few steps further as well by taking advantage of the fact that the data associated with each links survives the app installation process. This means that if the link was meant to deeplink to specific content (such as a product in an E-commerce service), you can take the user straight to the appropriate page once the new user completes the installation.
You can also consider using Dynamic Links to personalize the onboarding process for new users. For example, if you provide a referral bonus to users which relies on sharing of codes, you could ensure the link has this code already added as a parameter and add it automatically for the user right after install. This offers the opportunity to earn a lot of goodwill.
Links are only one part of the solution we have to offer - if you also use Firebase Invites, you can provide users with a simple options of choosing which contacts they would like to share your app with instead of relying upon third party apps. This list of contacts is also sorted to highlight those people the user frequently communicates with.
Firebase Invites referrals are sent over SMS and E-mails, and give you the benefits of Dynamic Links mentioned before. You can customize the invitation content, including the ability to specify templates with rich HTML content for E-mail. And you don't need the user to be signed in to your service or know their E-mail address either.
Your users are your best advocates, and we highly recommend minimizing the friction that might prevent them from sharing your application with the other people in their lives. Regardless of what your product is, you are likely to benefit from gently nudging - especially your more active ones.
Firebase Crash Reporting has enjoyed rapid adoption since its beta launch at Google I/O 2016. So far, we helped identify hundreds of millions of errors to help developers provide the best possible experience for users. Firebase Crash Reporting is now fully released, with many new features and enhancements to help you better diagnose and respond to crashes that affect the users of your iOS and Android mobile applications. Read on to discover what's new!
Issue Resolution
One of the most hotly requested features is the ability to mark an error cluster as "closed" in the dashboard, in order to indicate that the issue should be fixed, and that the next release should no longer generate that particular kind of crash. In the event of a regression in a future version, the crash cluster will be automatically reopened for context.
Improved Reporting Latency
The time it takes for a crash to be reported until the moment it appears in your console has been drastically decreased from about twenty minutes to less than a minute. We expect this improvement, in addition to email alerts, will improve your ability to diagnose errors as they happen.
Email Alerts
Anyone who has access to your Firebase project can arrange to receive an email alert if we see brand new clusters of errors, or errors that have regressed after being marked as closed. You can use this to quickly triage and respond to errors, in order to minimize the impact of a defect on your users.
Analytics events in Crash Logs
Firebase Analytics events are now added to your crash logs, which gives you a more complete view of the state of your app leading up to crash. This added context will also help you observe how crashes may be impacting your revenue and critical conversion events.
Mobile-Friendly Console
The Crash Reporting console has been improved for use on mobile devices. Its new responsive design makes it easy to check on the health of your apps when you're away from your desktop computer.
Android SDK Compatibility
The first release of the Android SDK had a limitation that prevented it from working well with some apps that declare an Application class. This limitation has been resolved, and Firebase Crash Reporting should work well with any Android app.
Updated Support for Swift on iOS
The service has been updated to show symbols from apps written in Swift 2 and 3.
We want your feedback!
If you decide to give Firebase Crash Reporting a try, please let us know how it went for you. For any questions about Crash Reporting or any other Firebase feature, please use the firebase-talk forum, or if it's a programming question, you can use the firebase tag on Stack Overflow.
Update: Both DebugView and StreamView are available to all Firebase developers, so you can get access to your data a whole lot faster! Happy Analytics viewing!
This is probably one of the most common questions we get around Firebase Analytics, and we thought it was worth taking some time to delve into this topic a little deeper. So buckle in, kids! We've got some learning ahead of us...
To understand latency with Firebase Analytics, there are two potential delays you need to be aware of:
Let's go over these one at a time.
Firebase Analytics doesn't constantly stream down data from the client device. That would be a serious battery drain, and we want to be respectful of your users' battery life. Instead, analytics data is batched up and sent down when the client library sees that there's any local data that's an hour old.
On iOS devices (and Android devices without Google Play Services), this one hour timer is per app. But on Android devices with Play Services, this one hour timer is across all apps using Firebase Analytics.
In addition, Firebase Analytics will send down all of its data from the client if your user triggers a conversion event (like making an in-app purchase). And on iOS devices, Firebase Analytics will also send down its data whenever your app goes into the background.
Not to worry. On most Android devices, it's Google Play Services that manages sending down this data. Which means that even if your user deletes your app after an hour, that data will still get sent down because Google Play Services still has it.
On iOS devices, Firebase will also send down data when your app moves into the background. So if a user tries your app for 20 minutes and then uninstalls it, that session data will still be received, because your app will have sent down the data the moment the user moved your app to the background. The one corner case where analytics data would actually get lost on iOS would be if your app crashed and then your user immediately uninstalled it.
Now, moving on to the second case, there's also the frequency at which Firebase Analytics grabs the latest batch of data it's received from your client and uses that data to update the reports and graphs you see in the Firebase Console. This process typically runs every few hours. So that's the kind of delay you should expect after your client data is sent down to the server.
If you're looking for your most recent data in these reports, keep in mind that the default "Last 30 days" report doesn't include any data from the current day -- this is because the data for the current day is incomplete, and it would be misleading (not to mention a bummer) to see every graph end with a giant downturn in usage. So if you want to see the current day's data, you'll want to select "Today" from the drop down menu in the Firebase Console.
On the other hand, if you've set up your app to export all of its Firebase analytics data to BigQuery, this data is available for you to look at right away. There are no batch reports that need to be run, so you can immediately view all of the day's data by looking at the app_events_intraday table that's automatically created for you in BigQuery. For more about this feature, be sure to check out our earlier blog post.
app_events_intraday
But outside of BigQuery, it generally takes a few hours for you to see any data you've recorded in Firebase Analytics.
As you may have heard at our Firebase Dev Summit, we've working on two enhancements to Firebase Analytics -- DebugView and StreamView -- which will give you more up-to-date insight into your analytics data during both development and production.
Neither of these are yet available to the general public, but as your reward for making it this far into the blog post, here's a link to sign up for the DebugView closed beta. See? Reading has its advantages!
There are many developers out there who want to be notified as soon as they see something unusual in their stats -- whether that's a sudden drop in in-app purchases, people failing to make it through the tutorial, or what-have-you.
And while you can't quite accomplish this with the free Firebase Analytics reports that you see in the console, you could accomplish this sort of thing by combining BigQuery with another tool such as Google Data Studio, a third-party visualization tool like Tableau, or even writing your own Google Apps Script monitoring script. All of which allow you to run some pretty sophisticated custom reports, but frankly, that's a whole other blog post.
Do keep in mind, however, that you're still subject to BigQuery usage charges when you query your data though these tools if you go beyond the 1TB/month free tier, so be mindful of how much (and how frequently) you decide to process your data.
Hopefully, this gives you a better understanding of how long it takes for you to see analytics data and what you can expect when you're developing your app. Now go forth and start recording those events!
If your app is using Firebase Realtime Database, you've probably gotten a lot of mileage out of its ability to notify your app quickly as changes are made to the database. Your listeners are triggering and receiving new data, users are delighted, and all is well with the world.
However, sometimes listeners may not behave in exactly the way you'd expect when combined with security and validation rules.
There are some important nuances to the way Firebase Realtime Database works, and how those nuances affect the way your listeners are triggered. Let's go over some of those situations, so you can expect the unexpected!
Are you using security and validation rules to protect access to your data? If not, please take a good hard look at that! But if you are using these rules, you can run into some behavior that may seem confusing at first, but is actually predictable, once you understand how the Firebase Realtime Database client libraries work.
All the code samples here will be in Java, because Android is my main thing. But the principles apply to each of the supported platforms, including iOS, web, and JavaScript on the server side.
Imagine you have the following database rules set up:
{ "rules": { ".read": true, ".write": false } }
So, basically, everything is readable and nothing is writable. Your security rules are likely going to be much more specialized, but the point is that some writes will not be allowed at certain locations or under certain circumstances. I'm keeping it simple here, in case you want to experiment with the code samples here in a new project.
Now imagine you have the following tiny bit of data in your database:
ROOT - data - value: 99
You'd expect that a ValueEventListener on the /data node would give you a snapshot containing a map of the key "value" to the number 99. So, if you executed this code, you'd get a single log statement showing these details:
private class MyValueEventListener implements ValueEventListener { @Override public void onDataChange(DataSnapshot dataSnapshot) { Log.i("********** change", dataSnapshot.getKey() + ": " + dataSnapshot.getValue()); } @Override public void onCancelled(DatabaseError databaseError) { // If we're not expecting an error, report it to your Firebase console FirebaseCrash.report(databaseError.toException()); } }
Pretty straightforward. But imagine you then attempt to change the value from 99 to 100:
HashMap map = new HashMap<>(); map.put("value", 100); dataRef.setValue(map);
Since our security rules prohibit this, we expect to fail. And it does. But one other thing happens that may not be expected. If MyValueEventListener is still registered at the time setValue() is called, it will also be triggered with the new value of 100. Not only that, but the listener will be triggered again with the original value of 99. Your app log might look something like this:
I/********** change: DataSnapshot { key = data, value = {value=99} } I/********** change: DataSnapshot { key = data, value = {value=100} } W/RepoOperation: setValue at /data failed: DatabaseError: Permission denied I/********** change: DataSnapshot { key = data, value = {value=99} }
So we see here that the listener got the original value of 99, then the updated value of 100, then an error, then back to the original 99.
Now, you might be thinking, "The security rules should have prevented that change to 100! What gives!" This is a completely understandable perspective. However, it's time to update your expectations with some knowledge about what's really going on here!
The client SDK has no knowledge of the security rules for your project. They live and are enforced on the Firebase server side. However, when the SDK handles the call to setValue(), it goes ahead and assumes that the update will actually work on the server. This is the usual case for code that's been written for a database with a particular set of rules —
the intent is typically never to violate any rules. With this assumption in play, the SDK goes ahead and acts early, as if the write to the database location has actually succeeded. The result of this is the triggering of all listeners currently added to the changed location within the same app process.
OK, so, you might be wondering: if a write can fail, why does the client SDK act early like this? The reasoning is that these immediate callbacks can help your app feel snappy in the face of a poor network connection, and also allows your app to be usable when completely offline. For example, if a user wants to make a change to their profile, why not let them see that change immediately, rather than having to wait for a full round trip to the server? After all, if your code intends to honor the security rules, there should be no problem, right?
In the case where your code does violate a security rule like this, the server notifies the app that the update actually failed at that location. The logical thing to do, at this point, is trigger all listeners at that location with the original data, so the UI of your app can regain consistency with known values from the server.
Given all this context on how security rules works, let's look at another scenario.
Child event listeners are different from the value event listeners described above. A ValueEventListener as shown above gives you the entire contents of a particular location, every time any part of it changes, whereas a ChildEventListener gives you callbacks for individual child nodes under a location whenever one of those children is added, changed, moved, or removed.
For this example, let's use the same security rules as before, with everything readable and nothing writable:
Now, let's say you have a node in your database called /messages, where you want users to be able to push new message content to be shared with others:
private class MyChildEventListener implements ChildEventListener { @Override public void onChildAdded(DataSnapshot dataSnapshot, String s) { Log.i("**********", "childAdded " + dataSnapshot.toString()); } @Override public void onChildChanged(DataSnapshot dataSnapshot, String s) { Log.i("**********", "childChanged " + dataSnapshot.toString()); } @Override public void onChildRemoved(DataSnapshot dataSnapshot) { Log.i("**********", "childRemoved " + dataSnapshot.toString()); } @Override public void onChildMoved(DataSnapshot dataSnapshot, String s) { Log.i("**********", "childMoved " + dataSnapshot.toString()); } @Override public void onCancelled(DatabaseError databaseError) { FirebaseCrash.report(databaseError.toException()); } } DatabaseReference messagesRef = FirebaseDatabase.getInstance().getReference("messages"); messagesRef.addChildEventListener(new MyChildEventListener()); HashMap map = new HashMap<>(); map.put("key", "value"); DatabaseReference newMesssageRef = newMessageRef.push(); newMessageRef.setValue(map);
In this code, we have a ChildEventListener added on /messages, then we're trying to add a new child object into a location determined by some generated push id. Of course, we expect this to fail because of the security rules. But, let's look at the log to see what actually happens if we execute this code:
I/**********: childAdded DataSnapshot { key = -KTfacNOAJt2fCUVtwtj, value = {key=value} } W/RepoOperation: setValue at /messages/-KTfacNOAJt2fCUVtwtj failed: DatabaseError: Permission denied I/**********: childRemoved DataSnapshot { key = -KTfacNOAJt2fCUVtwtj, value = {key=value} }
We see that the client library immediately triggers the onChildAdded method with the new child object under /messages, then logs an error, then triggers the onChildRemoved callback with the same object.
If you read through and understood the prior example, this one should be a little less surprising. The Firebase client SDK is again acting early in response to the call to setValue() and assuming that the write will success. Then, after the write fails because of the security rules, it attempts to "undo" the add that failed. This ensures that the app's UI can remain up-to-date with the correct child values, assuming that it has implemented onChildRemoved correctly.
The behavior of the Firebase client library in the face of violated security rules should be more clear now, but you might still be wondering how you can detect if a violation occurred. It may not be adequate for your app to simply reverse the effect of the write. In fact, you may even want to know if and when that actually happens, as it could be considered a programming error. This brings me to the next point.
In the examples above, it can be very difficult to tell if your call to setValue() failed at the server just by looking at the listener callbacks. If you want to detect failure, you'll need a bit of extra code to respond to that event. There are two ways to do this. First, there is CompletionListener that you can pass to an overload of setValue that gets notified of errors. Alternatively, you can also use the Play Services Task API by using the Task object returned by setValue. I'll prefer a Task here, because it has built-in protections against Activity leaks (note the first argument to addOnCompleteListener is an Activity instance):
Task task = messageRef.setValue(map); task.addOnCompleteListener(MainActivity.this, new OnCompleteListener() { @Override public void onComplete(@NonNull Task task) { Log.i("**********", "setValue complete"); if (!task.isSuccessful()) { Log.i("**********", "BUT IT FAILED", task.getException()); FirebaseCrash.log("Error writing to " + ref.toString()); FirebaseCrash.report(task.getException()); } } });
When the write of the value completes, with either success or error, the OnCompleteListener registered to the Task will be called. If it failed, I can check the Task to see if it was successful and deal with it as needed. In the above code, I'm choosing to report the error to Firebase Crash Reporting, which can help me determine if and where I made a mistake in my code or security rules. It's probably a good idea to always report your write failures like this, unless you fully expect that a write could legitimately fail, under normal circumstances, to a security rule.
To learn a lot more about the Task API, you can read a four-part blog series starting here.
When there is an update to a location that also has active listeners in the same process, the flow of data through the process goes like this:
Using this knowledge, it's possible you may have reset your expectations to expect the unexpected for your listeners! Were your expectations changed? Let me know in the comments below! And, if you have any programming questions about Firebase Realtime Database, you can ask us on Stack Overflow with the firebase-database tag. For more general questions, you can ask on Quora or use the firebase-talk Google Group.
If you like, follow me on Twitter as CodingDoug, and don't forget to check out our YouTube channel for Firebase tutorials and other shows.