WorryFree Computers   »   [go: up one dir, main page]

Posted by: Russell Ketchum

Analytics is the key to understanding your app's users: Where are they spending the most time in your app? When do they churn? What actions are they taking? To answer these questions, you need the right set of tools – and that’s why analytics has been a core part of Firebase since the beginning… and today, we’re excited to share with you that we’re adding more to our solution to help take app analytics to the next level!

Thanks to our continued partnership with Google Analytics, you can now upgrade your Firebase projects to the next generation of app analytics! This seamless upgrade requires no code changes and unlocks exciting new features while preserving data continuity. Now you can enjoy both the intuitive dashboards and free and unlimited event reporting you’re already enjoying in the Firebase console and all new capabilities to help you understand your user journeys in Google Analytics.

At a glance, here are some of the new capabilities and features you’ll be able to access in Google Analytics after making the upgrade:

  • Unlock insights with a more complete view of the customer journey. For the first time, measure customer journeys across devices, and get a unified picture of how your users interact with your content. For example, people often play games on many devices. They may start a game on their mobile phone during their commute, then pick it back up in the evening on their tablet, or maybe their friend’s device. You’ll want to understand journeys across these devices, and with new unified analytics you can do just that. You will need to implement the User-ID feature in analytics to measure across platforms and devices with deduplicated counts. Learn more about it here, or read on in the section below for how to enable cross-device reporting.
  • Create closed funnels to better understand paths to conversion. As we previewed at Google I/O 2019, this top-requested feature allows you to visualize the steps your users take with narrowing focus along each part of the journey so you can improve your conversion rates. The analytics dashboard in Firebase already includes funnels, but these are “open” funnels versus the closed funnels available in Google Analytics after the upgrade. With closed funnels you can answer questions like: “How many of my users browsed my store catalogue and compared items before making a purchase?”, or “At which step did my users drop off without making a purchase?” To learn more about the differences between open and closed funnels, check out this article.

    Understand how your users engage through key sequences in  your app using the Funnels technique in Google Analytics Understand how your users engage through key sequences in your app using the Funnels technique in Google Analytics


  • Learn more about what’s important to your users. Perform ad hoc analysis and use improved data visualization to get an even deeper understanding of what’s happening across various segments of users using the Analysis module.

    For example, want to know how behavior differs between new users of your app, 7-day actives, 30-day actives, and where their behavior overlaps? The new segment overlap tool will show you this data. Want to know how the behavior in those segments differ by geography? You can use geography and other variables as dimensions in Analysis and get that insight. You can then save and export those analyses to share with other members on your team.

Understand different types of users, and see where their behavior overlaps using the segment overlap visualization in the Exploration technique Understand different types of users, and see where their behavior overlaps using the segment overlap visualization in the Exploration technique
The Analysis module includes a number of useful techniques including Exploration, Funnels and Path analysis. Learn more.

  • Use all the same powerful app analytics capabilities you enjoy in Firebase. This upgrade is an additive experience requiring no code changes and provides data continuity. On top of the additional capabilities, you can continue to enjoy all your favorite Firebase and Analytics features, including free and unlimited event reporting in the Firebase console.

How to make the upgrade to the next generation Google Analytics experience

The process for upgrading your existing Firebase project to the next generation Google Analytics experience is easy. Just follow the steps below to make the upgrade:

  1. Log in to your Firebase console.
  2. Navigate to the Analytics dashboard.
  3. Click on the “Begin Upgrade” button in the banner:

    Games Dashboard

  4. Follow on-screen instructions to complete the upgrade.

From that point, you’ll be able to access these new features in Google Analytics.

The upgrade will be available to all Firebase users over the coming weeks.

Q: What will happen to my Firebase project analytics data after the upgrade? Will my analytics data still be available in the Firebase console?

      Yes! This upgrade enables a lot of new features and reports you will be able to access in Google Analytics, but will not affect your existing Firebase project analytics data in the Firebase console. You will be able to continue using the same dashboard and workflows you’re using today after the upgrade, but you’ll also have access to some advanced features in Google Analytics. Note that if you decide to enable cross-device reporting after making the upgrade, some user counts in your analytics data in the Firebase console may go down as the data will be de-duplicated with User-ID.

Q: Will I need to make any code changes to how I’m using the Firebase Analytics SDK today after making the upgrade?

      No, you will not need to make any changes to how you’re logging your analytics events using the Firebase Analytics SDK today, unless you want to take advantage of new unified cross-device reporting. If you want to take advantage of cross-device reporting after the upgrade, you’ll need to implement User-ID as mentioned earlier.

Q: Where will I be able to access these new analytics features after making the upgrade?


      All of the new features above will be available in Google Analytics after making the upgrade. You can directly access some of these new features via deep-links to Google Analytics from the Firebase console.

      Cross-device reporting and unified analytics with User-ID will be available in both Google Analytics and the Analytics dashboard in the Firebase console.

      To enable cross-device reporting for both, check out this article on implementing the User-ID in analytics. You can then update your Google Analytics settings in the Firebase console to view your reports using this property to enable cross-device reporting.

Q: I am migrating from the Google Analytics Services SDK, scheduled to sunset on October 31st, 2019. Does this upgrade impact that migration?

      No, the Google Analytics Services SDK migration is not affected by this upgrade, and you will still need to migrate to the Google Analytics for Firebase SDK to continue collecting your mobile application analytics data.

We are excited about the benefits this next generation app analytics experience will unlock for you, and look forward to hearing your feedback as you try out these new features!

Doug Stevenson
Developer Advocate
Firebase Test Lab logos illustration

The Firebase Test Lab team is pleased to announce that developers now have the ability to write Cloud Functions triggers that receive test results from Firebase Test Lab. Previously, developers had to manually check for test results, or write code to periodically poll the Test Lab API for test results. With Cloud Functions triggers, it becomes much easier to know immediately when a test finishes and get a summary of its results.

Firebase Test Lab enables you to run scripted and automated tests against your app on a wide variety of Android and iOS devices hosted in a Google data center. Cloud Functions now extends the capabilities of Test Lab by providing a fully managed backend to let developers write and deploy code that triggers when a test completes. Test Lab triggers written for deployment to Cloud Functions take the following form when using the Firebase SDK for JavaScript and deployed with the Firebase CLI:

exports.matrixOnComplete =
functions.testLab.testMatrix().onComplete(testMatrix => {
  const matrixId = testMatrix.testMatrixId;
  switch (testMatrix.state) {
    case 'FINISHED':
      // Test finished with results
      // Check testMatrix.outcomeSummary for pass/fail
      break;
    case 'ERROR':
      // Test completed with an infrastructure error
      break;
    // check other possible status codes...
  }
  return null;
});

You can use these triggers to programmatically notify your team of test results, for example, sending an email, posting a message to a Slack workspace, creating an issue in JIRA, as well as integrating with other team workflow tools.

Test Lab support appears in version 3.2.0 of the firebase-functions node module. Be sure to read the documentation to get more details for Test Lab triggers, and use the API reference to discover all the information available in the test matrix results. There is also a quickstart and sample code available in GitHub to help you get started. For discussion, join the Test Lab engineering team in the #test-lab channel on the Firebase Slack.

Patrick Martin
Developer Advocate
Firebase + Unity with logos

Firebase and Tasks

how to deal with asynchronous logic in Unity

The Firebase Unity SDK makes judicious use of asynchronous logic for many of its calls. Unity itself isn’t super resilient to threaded logic, with most of the classes and functions in the UnityEngine namespace just flat out throwing exceptions if invoked off of the main Unity thread. My goal with this post is to provide you the tools you need to not only safely use Firebase’s asynchronous function calls, but to do so in a way that best suits your own programming style and preferences. Ideally even giving you more confidence to thread other parts of your game to provide your players with the smooth and responsive gameplay they expect from a modern video game.

Let’s get started with a very innocent looking demo script:

using Firebase;
using Firebase.Auth;
using UnityEngine;
using UnityEngine.Assertions;

public class FirebaseContinueWith : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Checking Dependencies");
        FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask =>
        {
            Assert.IsNull(fixTask.Exception);
            Debug.Log("Authenticating");
            var auth = FirebaseAuth.DefaultInstance;
            auth.SignInAnonymouslyAsync().ContinueWith(authTask =>
            {
                Assert.IsNull(authTask.Exception);
                Debug.Log("Signed in!");
                var successes = PlayerPrefs.GetInt("Successes", 0);
                PlayerPrefs.SetInt("Successes", ++successes);
                Debug.Log($"Successes: {successes}");
                auth.SignOut();
                Debug.Log("Signed Out");
            });
        });
    }
}

The first thing I do is ensure that Firebase’s dependencies are available on the player’s device with CheckAndFixDependenciesAsync. Note that I’m not really handling any failure cases in this example. This shouldn’t be an issue for this post, but you’ll want to do more than assert in your own games.

Next I use ContinueWith to create a continuation and I start signing in anonymously with SignInAnonymouslyAsync.

When sign-in completes, I figure out how many times this script has run successfully before by reading PlayerPrefs. Then I increment this value, and write it back out before logging the new number of successes.

This is all super straightforward. I run it and… I just see the log “Signed In!” then nothing. What happened?

Threads

Firebase does a lot of work that’s dependent on I/O. This can either be out to disk, or even out to the network. Since you don’t want your game to lock up for potentially many seconds for network latency, Firebase uses Tasks to perform much of this I/O work in the background.

Whenever you continue from this work, you have to be careful to come back into your game in a graceful manner. I’ve done none of that here, and have just charged right into a shared resource managed by the UnityEngine in the form of a call to PlayerPrefs. This most likely raised an exception, but it even got lost in the background thread! What can you do to fix it?

Scheduling with Continuations

C# has the concept of a TaskScheduler. When you say ContinueWith, rather than just letting it continue on whatever thread the task completed on, you can use a TaskScheduler to force it onto a specific thread. So, I can modify the example to cache the TaskScheduler on which Start() was called. Then I pass that into the ContinueWith statement to be able to safely change the state of objects in my game:

Debug.Log("Checking Dependencies");
var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask =>
{
    Assert.IsNull(fixTask.Exception);
    Debug.Log("Authenticating");
    var auth = FirebaseAuth.DefaultInstance;
    auth.SignInAnonymouslyAsync().ContinueWith(authTask =>
    {
        Assert.IsNull(authTask.Exception);
        Debug.Log("Signed in!");
        var successes = PlayerPrefs.GetInt("Successes", 0);
        PlayerPrefs.SetInt("Successes", ++successes);
        Debug.Log($"Successes: {successes}");
        auth.SignOut();
        Debug.Log("Signed Out");
    }, taskScheduler);
});

Since Start executes on the Unity main thread, I grab the scheduler with TaskScheduler.FromCurrentSynchronizationContext(). This way I can get back to the main thread later by passing the scheduler into my second ContinueWith statement. Now whatever work I do in that ContinueWith block will be done in sequence with the game rather than in parallel with it, preventing any threading issues.

When I run the script, I can see that I finally have one success (and that this script hasn’t succeeded before).

This pattern is really common so Firebase provides an extension method named ContinueWithOnMainThread that does all of that hard work for you. If you’re using a newer version of the Firebase Unity SDK, you can write the above as simply:

Debug.Log("Checking Dependencies");
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask =>
{
    Assert.IsNull(fixTask.Exception);
    Debug.Log("Authenticating");
    var auth = FirebaseAuth.DefaultInstance;
    auth.SignInAnonymouslyAsync().ContinueWithOnMainThread(authTask =>
    {
        Assert.IsNull(authTask.Exception);
        Debug.Log("Signed in!");
        var successes = PlayerPrefs.GetInt("Successes", 0);
        PlayerPrefs.SetInt("Successes", ++successes);
        Debug.Log($"Successes: {successes}");
        auth.SignOut();
        Debug.Log("Signed Out");
    });
});

A word of caution as well. In C#, you can safely assume that anything in your ContinueWith block hasn’t been garbage collected. The same doesn’t hold true with Unity’s design. If you were to access any fields of this MonoBehaviour or its encompassing GameObject after OnDestroy is invoked, you would want to check that this hasn’t become null. Due to the way Unity implemented this as well, you cannot do so with the ?? operator.

Hopefully I’ve shed a little light on what’s happening in these tasks and continuations in Unity. You may also be a little frustrated now. What should be a simple block of code where we fix dependencies, sign on, then do work has become this ugly mess of nested statements that just becomes harder to read as we chain more steps into the logic. If only there were a better way!

Async/Await

Although the goal of tasks is to perform operations in parallel, so much logic in programming is sequential. Since continuations get hard to read, C# provides a mechanism in async/await syntax to represent this sequential logic. To use this mechanism, I’ll rewrite the Start method like this:

async void Start()
{
    Debug.Log("Checking Dependencies");
    await FirebaseApp.CheckAndFixDependenciesAsync();

    Debug.Log("Authenticating");
    var auth = FirebaseAuth.DefaultInstance;
    await auth.SignInAnonymouslyAsync();

    Debug.Log("Signed in!");
    var successes = PlayerPrefs.GetInt("Successes", 0);
    PlayerPrefs.SetInt("Successes", ++successes);
    Debug.Log($"Successes: {successes}");
    auth.SignOut();
    Debug.Log("Signed Out");
}

The first thing you’ll notice is that I denote Start as async. This tells the C# compiler “this function will perform work in the background. Do something else whilst it finishes up.”

Then, I replace ContinueWith with the await keyword. If I were doing anything with the Task’s result, I could store the result in a variable.

This reads much better, but why doesn’t my code break like the very first sample? It turns out that async functions will always return to the thread they’re awaited on. This way you don’t have to be as careful about thread safety in functions where you do this. In fact, by default, async functions that are awaited will typically execute on the thread that called them unless the developer explicitly did something else.

There is one downside compared to the ContinueWith sample though: the code following CheckAndFixDependenciesAsync will execute on the Unity main thread rather than potentially running on a background thread. In practice, this won’t be much of an issue. It could be a behaviour of note if you’re doing some significant amount of work between calls to await. Be aware as well that this code is very similar to the continuation example above. Just like how Unity may clean up your underlying MonoBehaviour before ContinueWith executes, Unity may clean it up when the call to await completes. If you access any member fields after a call to await, you should check to ensure this is not yet null.

The Unity Way

Unity has the concept of coroutines, which used to be the preferred method of performing asynchronous work across multiple frames. The interesting bit about Coroutines is that they’re not really asynchronous, behind the scenes they simply generate IEnumerators which are evaluated on the main thread.

Unity has some special yield instructions such as WaitForEndOfFrame and WaitForSeconds, allowing you to jump around to different moments in your game’s time. I choose to implement a new CustomYieldInstruction to wait for a task to complete. I’ve even seen some developers convert something like this into an extension method on the Task class itself!

using System.Threading.Tasks;
using UnityEngine;

public class YieldTask : CustomYieldInstruction
{
    public YieldTask(Task task)
    {
        Task = task;
    }

    public override bool keepWaiting => !Task.IsCompleted;

    public Task Task { get; }
}

I can now use yield return on a Task, such as the Tasks typically used for Firebase, to make my asynchronous logic read sequentially. Inside it, I wait for a task to complete with a standard continuation. If I were actually doing something with the result of a Task, I’d have to build this class out a little more. For the time being, this will work to illustrate my basic point.

I can then reimplement my async/await logic using coroutines like this:

using System.Collections;
using System.Collections.Generic;
using Firebase;
using Firebase.Auth;
using UnityEngine;

public class FirebaseCoroutine : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(DoWork());
    }

    private IEnumerator DoWork()
    {
        Debug.Log("Checking Dependencies");
        yield return new YieldTask(FirebaseApp.CheckAndFixDependenciesAsync());

        Debug.Log("Authenticating");
        var auth = FirebaseAuth.DefaultInstance;
        yield return new YieldTask(auth.SignInAnonymouslyAsync());

        Debug.Log("Signed in!");
        var successes = PlayerPrefs.GetInt("Successes", 0);
        PlayerPrefs.SetInt("Successes", ++successes);
        Debug.Log($"Successes: {successes}");
        auth.SignOut();
        Debug.Log("Signed Out");
    }
}

My Start function now just immediately calls a function called DoWork, which is my coroutine.

Since I’m not doing anything with the return value of the async function calls, I just allocate temporary YieldTask objects and return them in the DoWork coroutine.

Now there are some important pros and cons to consider if you use this type of logic. This will have a performance hit as not only does the work between each yield return call execute on the main thread, but the property keepWaiting is queried every frame. On the other hand coroutines only exist for as long as a MonoBehaviour hasn’t been destroyed. This means that those caveats I mentioned above with having to check for null after an await or inside a ContinueWith don’t apply to coroutines!

Queues, Queues Everywhere!

Sometimes the performance characteristics of coroutines don’t match up exactly to what you want. Remembering that a CustomYieldInstruction is queried every frame, you may end up in a state where Unity is performing many checks against against the keepWaiting property. In this case, it may be beneficial to queue these actions on Unity thread manually by adding work to a queue when it’s ready to be processed. Note that this is effectively how ContinueWithOnMainThread works and you should use that method when possible.

With that in mind, let’s look at an example of how I’ve implemented an action queue:

using System;
using System.Collections.Generic;
using System.Linq;
using Firebase;
using Firebase.Auth;
using UnityEngine;
using UnityEngine.Assertions;

public class FirebaseQueue : MonoBehaviour
{
    private Queue<Action> _actionQueue = new Queue<Action>();

    void Start()
    {
        Debug.Log("Checking Dependencies");
        FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(fixTask =>
        {
            Assert.IsNull(fixTask.Exception);
            Debug.Log("Authenticating");
            var auth = FirebaseAuth.DefaultInstance;
            auth.SignInAnonymouslyAsync().ContinueWith(authTask =>
            {
                EnqueueAction(() =>
                {
                    Assert.IsNull(authTask.Exception);
                    Debug.Log("Signed in!");
                    var successes = PlayerPrefs.GetInt("Successes", 0);
                    PlayerPrefs.SetInt("Successes", ++successes);
                    Debug.Log($"Successes: {successes}");
                    auth.SignOut();
                    Debug.Log("Signed Out");
                });
            });
        });
    }

    public void EnqueueAction(Action action)
    {
        lock (_actionQueue)
        {
            _actionQueue.Enqueue(action);
        }
    }

    void Update()
    {
        while (_actionQueue.Any())
        {
            Action action;
            lock (_actionQueue)
            {
                action = _actionQueue.Dequeue();
            }

            action();
        }
    }
}

This starts much like all of my continuation based examples. Unlike those, I call EnqueueAction to perform work on the main thread. I would highly recommend breaking this into two different MonBehaviours so you don’t forget to call EnqueueAction, but I’m compressing this for illustrative reasons.

The EnqueueAction function locks the Queue and adds some nugget of logic in the form of a C# Action into a list of logic to be executed later. If you get really clever, you may be able to just replace all of this with a lockless thread safe queue.

Finally, in Update, I execute every enqueued Action. It is very important to NOT execute the action with the _actionQueue locked. If the Action itself enqueues another Action, you’ll end up in a deadlock.

Similar to the coroutine, this does involve checking whether or not the queue is empty every frame. Using what you’ve learned above about task schedulers and coroutines, I’m confident that you could reduce this burden with little effort if this becomes an issue.

Prescription: Unity! UniRx

Finally, there is a hip concept running around many programming circles known as reactive programming. Game developers I talk to tend to either love this or hate it, and there is enough public discourse that I won’t spend this post trying to turn you for or against this paradigm.

Reactive programming tends to favor logic that can come in streams -- that is logic that you would typically register for an event or query something every frame for -- and where you’ll perform functional operations on the streams as they flow through your game. For the purpose of staying consistent with the rest of this post, I’ll use it with the current example with the note that I’m not giving reactive programming its chance to shine.

So, with all that said, first I import UniRx from the Unity Asset Store. Then I have to make sure that there’s a MainThreadDispatcher in my scene:

inspector, MainThreadDispatch box

Now I can write my logic in UniRx form:

using Firebase;
using Firebase.Auth;
using UniRx;
using UnityEngine;

public class FirebaseRx : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Checking Dependencies");
        FirebaseApp.CheckAndFixDependenciesAsync().ToObservable().Subscribe(status =>
        {
            Debug.Log("Authenticating");
            var auth = FirebaseAuth.DefaultInstance;
            auth.SignInAnonymouslyAsync().ToObservable().ObserveOnMainThread().Subscribe(user =>
            {
                Debug.Log("Signed in!");
                var successes = PlayerPrefs.GetInt("Successes", 0);
                PlayerPrefs.SetInt("Successes", ++successes);
                Debug.Log($"Successes: {successes}");
                auth.SignOut();
                Debug.Log("Signed Out");
            });
        });
    }
}

What’s interesting about UniRx is that I can compose behaviour to form complex interactions. A brief example of this is the call ObserveOnMainThread, which guarantees that the following Subscribe executes on the main thread.

For an example like this one, I would not pull in the complexities of UniRx but it’s useful to put it on your radar. If you were instead trying to build game logic around realtime database updates or periodically invoking cloud functions based on streams of events in game, you could do worse than combining UniRx and Zenject to quickly build a robust system around asynchronous logic.

Conclusion

I hope that I’ve not only given you some tools to help understand Firebase’s asynchronous API, but have empowered you to deal with them in a way that best suits your own game and coding style. I would strongly encourage you to create a small project using each of the techniques I’ve outlined here to really get a feel for the shape of each solution, and encourage you to think about ways you might improve your game’s performance using threads elsewhere. I’ve personally gotten some great mileage out of background tasks when dealing with peer to peer communication in games as well as processing and annotating screenshots without halting gameplay. I’ve even found that sometimes things like enemy AI doesn’t actually need to finish processing every frame, and it can sometimes be perfectly fine to let it run for a bit in the background over the course of a few frames.