This article shows you how to combine:
a cloud code script (triggered by an API post-hook)
a user statistic (to keep track of when a user's data was last saved)
a custom entity collection (for the user backups)
to create a simple backup system for your app's user account data.
Note that this solution uses Custom Entities - and thus requires a brainCloud Plus plan. Refer to the linked article for how to update your plan -- https://help.getbraincloud.com/en/articles/3275913-changing-your-plan
Also note that usage of Custom Entities incurs Deep Data usage pricing. The cost of backing up player accounts varies according to the average amount of data per player, the time between backups, and the backup retention period. The good news is that this approach only backs up active accounts -- no storage is wasted on users that have left your app.
---
How it works
The script will run each time a user authenticates
The script will check to see when the user's data was last backed up - and if it was more than 24 hours ago - it will export a copy of the user's data and store in a custom entity
The user backups will be deleted automatically after 30 days
With this system in place - it will be possible to view old copies of a user's data - useful for debugging and/or recovering a user's account in the event of an app (or user) failure of some kind.
Caveat - restoring the backed-up data is a manual process. Devs will need to carefully copy-and-paste the data into the right entities for the user to restore the account. If you have questions, you can always contact brainCloud support for assistance.
---
Setting it up
We create the system in 5 steps:
Create the
playerBackup
Custom Entity typeCreate the
lastBackup
User StatisticCreate the
authPosthook_backupPlayerAccount
Cloud Code scriptConfigure the API Hook
Test
Step 1: Create the playerBackup Custom Entity type
Remember: you need to be on a Plus plan for Custom Entities.
It's very important that this solution use Custom Entities:
User Entities aren't suitable because they are owned by a user, and will be deleted if the user account is deleted... (and thus don't protect against accidental account deletion!). Also - User Entities do not support TTL.
Global Entities aren't suitable because they are meant for smaller collections of objects - normally less than 1000. They won't scale to the levels you need for this solution - where thousands (even millions!) of users may have multiple backups each within a 30 day period of time.
To create the Custom Entity collection:
Go to Design | Cloud Data | Custom Entities
Click [+ Create Entity Type]
Enter playerBackup for the Name
Enter identifier for the Identifier
Ensure that Owned is set to false. This is important so that the backup exists even if the user is deleted - and also so that your backups to do include other backups! Migrate should also be false.
You might want to set Notes to Daily backups of active players
The rest of the values can be left as is
Click [Create] to create the entity type collection
Then create three indexes for easy searching:
Create the profileId + saveDate index
Set Name to
profileId_1_saveDate_1
Set Keys to
{ "data.profileId": 1, "data.saveDate": 1 }
Create the profileId + createdAt index
Set Name to
profileId_1_createdAt_min1
Set Keys to
{ "data.profileId": 1, "createdAt": -1 }
Create an optional email + createdAt index
Set Name to
email_1_createdAt_min1
Set Keys to
{ "data.emailAddress": 1, "createdAt": -1 }
Set Options to the JSON settings below
{
"partialFilterExpression": {
"data.emailAddress": {
"$exists": true
}
}
}
---
Step 2: Create the lastBackup User Statistic
The script uses a user statistic
to record the last backup time in UTC millisecond
s. Be sure to adjust the Maximum Value that statistic because it is not large enough for our purposes.
In the Design Portal:
Go to Design | Statistics Rules | User Statistics
Click the [+]
Enter lastBackup as the Name
Enter backup for the Category
Ensure that Type is set to Long
Enter
32503698000373
as the Maximum Value (that will get you to the year 3000!)For Description, put Time in milliseconds of the user's last backup
The rest of the values can be left as is.
Click [Save] to create the statistic
---
Step 3: Create the authPosthook_backupPlayerAccount
Cloud Code script
Now for the meat of the solution. The script will perform an export of the player's account, and store it in a custom entity for safe keeping.
Before performing the export, the script will first check to see when the last backup was performed - and if it was less than 24 hours ago (86,400,000
millis) - it will skip the backup. The playerBackup entity itself will be created with a TTL (Time to Live) of 30 days (i.e. 2,592,000,000
millis) - after which it will be automatically deleted. These values have been implemented as constants in the script and can be tweaked for your app's requirements.
To create the script:
Go to Design | Cloud Code | Scripts
Click Actions | Create Script
Enter
authPostHook_backupPlayerAccount
as the Script NameEnter Perform daily player backup as the Description
You can leave the rest of the defaults as is
Then switch to the Editor tab - and copy-and-paste the following code:
Click [Save] to save the script
"use strict";
function getFieldSafely( parentObject, fieldName, valueIfAbsent ) {
if (parentObject.hasOwnProperty(fieldName)) {
return parentObject[fieldName];
} else {
return valueIfAbsent;
}
}
function main() {
var response = {};
const BACKUP_INTERVAL = 86400000; // 24 hours
const TTL = 2592000000; // 30 days
const ENTITY_COLLECTION = "playerBackup";
const LAST_BACKUP_STAT = "lastBackup";
var message = data.message;
// Use the authenticate response (i.e. data.message)
// to get some of the values we need
var currentTime = data.message.server_time;
var lastBackupMillis = getFieldSafely(
data.message.statistics, LAST_BACKUP_STAT, 0);
// Do we need to perform a backup?
if ( (currentTime - lastBackupMillis) < BACKUP_INTERVAL ) {
// No. Just exit early
message.backupMessage = "Skipping backup (recently backed up)";
response.status = 200;
response.data = message;
return response;
} else {
// Perform the backup
var customEntityProxy = bridge.getCustomEntityServiceProxy();
var playerStatisticsProxy =
bridge.getPlayerStatisticsServiceProxy();
var userProxy = bridge.getUserServiceProxy();
var optionsJson = { "includeEntities": true };
var profileId = data.message.profileId;
// Update the stat before we do export - to prevent
// concurrency issues.
var stat_lastBackup = {};
stat_lastBackup[LAST_BACKUP_STAT] = "SET#"+currentTime;
playerStatisticsProxy.processStatistics(stat_lastBackup);
var userExport =
userProxy.sysGetUserExport(profileId, optionsJson);
if (userExport.status == 200) {
var saveDate = new Date().toISOString().slice(0, 10);
// Format the identifier
var identifier = "";
var playerName = getFieldSafely(
data.message, "playerName", "" );
var emailAddress = getFieldSafely(
data.message, "emailAddress", null);
if ((playerName !== null) && (playerName !== "")) {
if ((emailAddress !== null) && (emailAddress !== "")) {
identifier = playerName +
" (" + emailAddress + ") - " + saveDate;
} else {
identifier = playerName +
" (" + profileId + ") - " + saveDate;
}
} else {
if ((emailAddress !== null) && (emailAddress !== "")) {
identifier = emailAddress + " - " + saveDate;
} else {
identifier = profileId + " - " + saveDate;
}
}
var dataJson = {
"profileId": profileId,
"saveDate" : saveDate,
"identifier": identifier,
"exportData": userExport.data
};
// Only write the email address if it is set...
if ((emailAddress !== "") && (emailAddress !== null)) {
dataJson.emailAddress = emailAddress;
}
var backupResult =
customEntityProxy.createEntity(
ENTITY_COLLECTION, dataJson, {"other": 1}, TTL, false);
if (backupResult.status == 200) {
message.backupMessage = "new backup entity " +
backupResult.data.entityId +
" saved in custom entity collection " +
ENTITY_COLLECTION;
} else {
message.backupMessage =
"Unexpected error backing up player data";
message.backupResult = backupResult;
// Set lastBackup time back to what it was previously
stat_lastBackup[LAST_BACKUP_STAT] = "SET#"+lastBackupMillis;
playerStatisticsProxy.processStatistics(stat_lastBackup);
}
} else {
message.backupMessage =
"Unexpected error exporting player data";
message.backupResult = userExport;
// Set lastBackup time back to what it was previously
stat_lastBackup[LAST_BACKUP_STAT] = "SET#"+lastBackupMillis;
playerStatisticsProxy.processStatistics(stat_lastBackup);
}
response.status = 200;
response.data = message;
return response;
}
}
main();
---
Step 4: Configure an API Hook to call the script
You need to hook this script up to the Authentication call. To do so:
Go to Design | Cloud Code | API Hooks
Click [+ Create]
Select Authenticate for Service
Select Authenticate for Operation
Select Post for Pre/Post
Select
authPostHook_backupPlayerAccount
for ScriptRemove
"key": "value"
from the Params field (leave just the empty braces)Click [Save]
---
Step 5: Test it!
We should now be all set.
Go to the API Explorer (Design | Cloud Code | API Explorer), select the Authenticate Service and Authenticate Operation - and click [Run].
You should see a new "backupMessage" field in the Authentication results that indicates that a backup of the player data was created.
Click [Run] again - and this time the message will say that the backup was skipped - because a recent backup already exists.
---
Searching for backups
Now that backups are being created, how do you search for them?
It is pretty simple:
Go to Monitoring | Global Monitoring | Custom Entities
Click on the playerBackup entity type
Click on the [Filter] button, and then enter a filter query - and click [Apply]
Here are a few simple examples:
Search for all backups for a given player (i.e. profileId):
{
"data.profileId": "aaa-bbb-ccc"
}
Search for backups for the specific player on a specific day:
{
"data.profileId": "aaa-bbb-ccc",
"data.saveDate": "2022-04-29"
}
Search for all backups for a given email address:
{
"data.emailAddress": "bruce@waynetech.com",
"data.saveDate": "2022-04-29"
}
Note - you can search for other player data of course - but be sure to create indexes that cover the fields of your search.
If you have questions - please reach out to our Support team!