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 =;
//If no stats exist yet create an empty map
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]
typeMap = {}

//Inner function loops of the events per type
Object.keys(inputTypeMap).map(function(key) {
var value = inputTypeMap[key];
var count = typeMap[key]
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
return false
val serverURL: String = BASE_API_URL + “/uploadStats”
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 =
val outputStream = DataOutputStream(connection.outputStream)
Log.d(TAG, “Response code: “ + connection.responseCode)
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
val reader: BufferedReader =
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()
.setTrigger(Trigger.executionWindow(windowStart, windowStart + toleranceInterval))
class AppUpdatedReceiver : BroadcastReceiver() {@CallSuper
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, “app updated..”)
<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 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: ‘’,
subject: ‘Your daily email overview!’,
html: html,
‘h:Reply-To’: ‘’,
to: ‘’
var mailgun = require(‘mailgun-js’)({apiKey : mailGunApiKey, domain : mailGunDomain})return mailgun.messages().send(data, function (error, 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
Rob J

Rob J

Freelance Android Developer since 2012 🎙️Host of 🌍 World Tourist ☕ Coffee Addict |