Skip to content

Instantly share code, notes, and snippets.

@felquis
Last active March 13, 2026 22:36
Show Gist options
  • Select an option

  • Save felquis/dead50b6f96bc3e989aa03965aa47cd7 to your computer and use it in GitHub Desktop.

Select an option

Save felquis/dead50b6f96bc3e989aa03965aa47cd7 to your computer and use it in GitHub Desktop.
chrome web store analytics pro - get daily install vs uninstall conversation rate.

What does it do?

Compare conversion percentage between installs vs uninstalls.

image

Why is this not a Chrome Extension?

Chrome does not allow extensions to use content scripts in chrome.google.com, it may be due to security concerns

How to use this script

You need to copy and paste the script above in the correct page, only works in the last 30 days view (for now 👀)

Step 1:

Visit the "analytics > Install and Uninstall"

Step 2:

copy the script and page it in the dev tools

PS: it's a fairly small script, and you can double check it with any AI tool, it doesn't send network requests, etc


If you know a way to allow a chrome extension to have access to manipulate the DOM via content scripts, I would like to make this a chrome extension 🙏

(() => {
const TARGET_CLASS_PATTERN = /^ds:\d+$/;
const TARGET_KEYS = new Set(["ds:5", "ds:6"]);
function getTargetScripts(root = document) {
return Array.from(root.querySelectorAll("script[class]"))
.filter((scriptEl) =>
Array.from(scriptEl.classList).some((className) => TARGET_CLASS_PATTERN.test(className))
);
}
function extractCallbacks(scriptText) {
const callbacks = [];
const callbackPattern = /AF_initDataCallback\((\{[\s\S]*?\})\);/g;
let match;
while ((match = callbackPattern.exec(scriptText)) !== null) {
callbacks.push(match[1]);
}
return callbacks;
}
function parsePayload(callbackText) {
const keyMatch = callbackText.match(/key:\s*'([^']+)'/);
if (!keyMatch) return null;
const key = keyMatch[1];
if (!TARGET_KEYS.has(key)) return null;
const dataMatch = callbackText.match(/data:\s*(\[[\s\S]*\])\s*,\s*sideChannel\s*:/);
if (!dataMatch) return null;
let data;
try {
data = JSON.parse(dataMatch[1]);
} catch {
return null;
}
return { key, data };
}
function extractSeriesRows(dataBlock) {
const rows = Array.isArray(dataBlock?.[1]) ? dataBlock[1] : [];
return rows
.map((row) => {
const date = row?.[0];
const bucket = row?.[1]?.[0]?.[1]?.[0];
if (typeof date !== "string") return null;
if (!Array.isArray(bucket)) return null;
if (typeof bucket[1] !== "number") return null;
if (typeof bucket[2] !== "string") return null;
return {
date,
value: bucket[1],
metric: bucket[2]
};
})
.filter(Boolean);
}
function sumValues(series) {
return series.reduce((acc, item) => acc + item.value, 0);
}
function mergeByDate(installsSeries, uninstallsSeries) {
const byDate = new Map();
installsSeries.forEach((item) => {
const current = byDate.get(item.date) || { date: item.date, installs: 0, uninstalls: 0, net: 0 };
current.installs = item.value;
current.net = current.installs - current.uninstalls;
byDate.set(item.date, current);
});
uninstallsSeries.forEach((item) => {
const current = byDate.get(item.date) || { date: item.date, installs: 0, uninstalls: 0, net: 0 };
current.uninstalls = item.value;
current.net = current.installs - current.uninstalls;
byDate.set(item.date, current);
});
return Array.from(byDate.values())
.map((row) => {
const conversionPercent = row.installs > 0
? (row.net / row.installs) * 100
: null;
return {
...row,
conversionPercent
};
})
.sort((a, b) => a.date.localeCompare(b.date));
}
function averageConversionPercent(rows) {
const values = rows
.map((row) => row.conversionPercent)
.filter((value) => typeof value === "number");
if (!values.length) return null;
return values.reduce((acc, value) => acc + value, 0) / values.length;
}
function analyze(root = document) {
const scripts = getTargetScripts(root);
const analyticsByKey = new Map();
scripts.forEach((scriptEl) => {
const callbacks = extractCallbacks(scriptEl.textContent || "");
callbacks.forEach((callbackText) => {
const payload = parsePayload(callbackText);
if (!payload) return;
analyticsByKey.set(payload.key, extractSeriesRows(payload.data));
});
});
const installsSeries = analyticsByKey.get("ds:5") || [];
const uninstallsSeries = analyticsByKey.get("ds:6") || [];
const summary = {
scriptsScanned: scripts.length,
installsRows: installsSeries.length,
uninstallsRows: uninstallsSeries.length,
installsTotal: sumValues(installsSeries),
uninstallsTotal: sumValues(uninstallsSeries)
};
summary.netTotal = summary.installsTotal - summary.uninstallsTotal;
const byDate = mergeByDate(installsSeries, uninstallsSeries);
summary.overallConversionPercent = summary.installsTotal > 0
? (summary.netTotal / summary.installsTotal) * 100
: null;
summary.averageDailyConversionPercent = averageConversionPercent(byDate);
return {
summary,
installsSeries,
uninstallsSeries,
byDate
};
}
function print(result) {
const data = result || analyze(document);
const byDateForPrint = data.byDate.map((row) => ({
...row,
conversionPercent: row.conversionPercent === null
? null
: Number(row.conversionPercent.toFixed(2))
}));
const summaryForPrint = {
...data.summary,
overallConversionPercent: data.summary.overallConversionPercent === null
? null
: Number(data.summary.overallConversionPercent.toFixed(2)),
averageDailyConversionPercent: data.summary.averageDailyConversionPercent === null
? null
: Number(data.summary.averageDailyConversionPercent.toFixed(2))
};
console.log("[WebStoreAnalytics] Summary", summaryForPrint);
if (byDateForPrint.length) {
console.table(byDateForPrint);
} else {
console.warn("[WebStoreAnalytics] No ds:5/ds:6 data found in script tags.");
}
return data;
}
const api = {
run: () => analyze(document),
print: (result) => print(result)
};
window.WebStoreAnalytics = api;
console.log("[WebStoreAnalytics] Ready. Run: WebStoreAnalytics.print()");
WebStoreAnalytics.print();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment