How I got Android App Installs + IAP numbers mailed to my Inbox

Putting the pieces in place

  • a database component to store the information
  • Some server side API component to collate that data submitted across multiple installs throughout my user base
  • A way to email that data to my inbox
  • And an email template to display the data
  • Oh and I’d need to build a client side SDK I could integrate across all my apps to make my life a little easier.

The API layer

exports.uploadStats = functions.https.onRequest((req, res) => {  return uploadStats(req, res) //This function is detailed next});
{
some_type: {
some_event: number_of_occurences
},
some_other_type: {
some_event: number_of_occurences
},
...
}
var packageName = req.get(‘packageName’)
var docRef = admin.firestore().doc("appStats/" + packageName)
return docRef.get().then(snap => {
var appStats = snap.data();
//If no stats exist yet create an empty map
if(isNull(appStats))
appStats = {}
...});
//This function loops over the types
Object.keys(inputStats).map(function(type) {
var inputTypeMap = inputStats[type];
if(isNotNull(inputTypeMap) && isNotEmptyMap(inputTypeMap)) {
var typeMap = appStats[type]
if(isNull(typeMap))
typeMap = {}

//Inner function loops of the events per type
Object.keys(inputTypeMap).map(function(key) {
var value = inputTypeMap[key];
var count = typeMap[key]
if(isNull(count))
count = 0
count = count + value
typeMap[key] = count
}); appStats[type] = typeMap
}
});
return docRef.set(appStats)...

Building the SDK

internal fun uploadStats(context: Context) : Boolean {
//The user has the option to disable all analytics/crashlytics from the ui
if(!SdkUtils.getCollectionEnabled(context))
return false
val serverURL: String = BASE_API_URL + “/uploadStats”
val
url = URL(serverURL)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = “POST”
//30 seconds because Triggers can be slow to start/return
connection.connectTimeout = 300000
connection.connectTimeout = 300000
connection.doOutput = true//This method simply loops through my shared preferences & puts them
//into a map of type of:
//Map<String(type), Map<String(key), Long(count)>>
val map = SdkUtils.buildMap(context)
val json = JSONObject(map)
val postData: ByteArray =
json.toString().toByteArray(Charsets.UTF_8)
connection.setRequestProperty(“Content-length”,
postData.size.toString())
val outputStream = DataOutputStream(connection.outputStream)
outputStream.write(postData)
outputStream.flush()
Log.d(TAG, “Response code: “ + connection.responseCode)
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
val reader: BufferedReader =
BufferedReader(InputStreamReader(connection.errorStream))
val output: String = reader.readLine()
Log.d(TAG,”Api threw error $output)
return false
}
return true
}
launch {  uploadStats(context)
StatManager.clear(context) //Clears daily stats regardless of success
}
val dispatcher = FirebaseJobDispatcher(GooglePlayDriver(context));val myJob = dispatcher.newJobBuilder()
.setService(UploadService::class.java)
.setTag(UploadService::class.java.simpleName)
.setRecurring(true)
.setLifetime(Lifetime.FOREVER)
.setTrigger(Trigger.executionWindow(windowStart, windowStart + toleranceInterval))
.setReplaceCurrent(true)
.setRetryStrategy(RetryStrategy.DEFAULT_LINEAR)
.setConstraints(Constraint.ON_ANY_NETWORK)
.build();
dispatcher.mustSchedule(myJob);
class AppUpdatedReceiver : BroadcastReceiver() {@CallSuper
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, “app updated..”)
SubmissionManager.scheduleUpload(context)
...
<intent-filter> <action android:name=”android.intent.action.MY_PACKAGE_REPLACED” /></intent-filter>

Building the email

..header of the email..{{#eachInMap this}} //Top level type   {{key}} //Create type title with key     {{#eachInMap value}} //Nest event:value for each type       Event: {{key}}       Count: {{value}}     {{/eachInMap}}{{/eachInMap}}..footer of the email..
var handlebars = require(‘handlebars’)
handlebars.registerHelper( ‘eachInMap’, function ( map, block ) {
var out = ‘’;
Object.keys( map ).map(function( prop ) {
out += block.fn( {key: prop, value: map[ prop ]} );
});
return out;
});
exports.sendEmail = functions.https.onRequest((req, res) => {   var packageName = req.query.package_name
var docRef = admin.firestore().doc(appStatsPath + packageName)
return docRef.get()...
if(isNull(appStats))
//If empty I throw a 400 response
var fs = require(‘fs’); //Filesystemvar source = fs.readFileSync(“./email_template.html”,”utf-8");const template = handlebars.compile(source, { strict: true });var html = template(appStats);
var data = {
from: ‘noreply@droidstats.com’,
subject: ‘Your daily email overview!’,
html: html,
‘h:Reply-To’: ‘my@email.address.com’,
to: ‘my@email.address.com’
}
var mailgun = require(‘mailgun-js’)({apiKey : mailGunApiKey, domain : mailGunDomain})return mailgun.messages().send(data, function (error, body) {
console.log(body)
send(res, 200, { message: ‘Success’, body, });
});

And that’s how it’s done!

DroidStats email for Coffee-Working

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store