|
(() => { |
|
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(); |
|
})(); |