It is recommended to run this analysis only once per hour.",
"Yes, Continue"
);
if (!userConfirmed) {
UI.log("Analysis cancelled by user.");
console.log("Analysis cancelled by user.");
if (btnRun) btnRun.disabled = false;
return;
}
UI.setStatus("Analyzing...");
UI.log("Starting deep analysis...");
const logTab = document.querySelector('[data-target="ig-log"]');
if (logTab) logTab.click();
try {
const userId = Utils.getUserId();
if (!userId) throw new Error("User ID could not be obtained. Are you logged in?");
UI.log("Fetching 'Following'...");
const followingDetailedRaw = await API.getAllUsers(userId, CONFIG.FOLLOWING_HASH, "following");
UI.log("Fetching 'Followers'...");
const followersDetailedRaw = await API.getAllUsers(userId, CONFIG.FOLLOWERS_HASH, "followers");
const followingDetailed = Utils.toDetailedUserArray(followingDetailedRaw);
const followersDetailed = Utils.toDetailedUserArray(followersDetailedRaw);
const following = followingDetailed.map((u) => u.username);
const followers = followersDetailed.map((u) => u.username);
UI.hideProgress();
UI.setStatus("Calculating Metrics...");
Storage.addHistoryEntry(followers.length, following.length);
UI.renderHistory(Storage.getHistory());
const notFollowingBackUsernames = Utils.diff(following, followers);
const fansUsernames = Utils.diff(followers, following);
const mutualsUsernames = Utils.intersection(followers, following);
const whitelist = Storage.getWhitelist();
const filteredNotFollowing = notFollowingBackUsernames.filter((u) => !whitelist.includes(u));
const mapToDetailed = (arr) => arr.map((u) => ({ username: u, url: "https://www.instagram.com/" + u + "/" }));
const notFollowingBackDetailed = mapToDetailed(filteredNotFollowing);
const fansDetailed = mapToDetailed(fansUsernames);
const mutualsDetailed = mapToDetailed(mutualsUsernames);
UI.log("Not Following Back (filtered): " + notFollowingBackDetailed.length);
UI.log("Fans: " + fansDetailed.length);
UI.log("Mutuals: " + mutualsDetailed.length);
const prev = Storage.load();
if (prev) {
const prevFollowersDetailed = Utils.toDetailedUserArray(
Array.isArray(prev.followersDetailed) ? prev.followersDetailed : prev.followers || []
);
const prevFollowingDetailed = Utils.toDetailedUserArray(
Array.isArray(prev.followingDetailed) ? prev.followingDetailed : prev.following || []
);
const prevFollowers = prevFollowersDetailed.map((u) => u.username);
const prevFollowing = prevFollowingDetailed.map((u) => u.username);
const newFollowers = Utils.diff(followers, prevFollowers);
UI.log("New followers since last run: " + newFollowers.length);
const lostFollowers = Utils.diff(prevFollowers, followers);
const lostFollowing = Utils.diff(prevFollowing, following);
const missingUsers = Utils.intersection(lostFollowers, lostFollowing);
const prevMutualsDetailed = Utils.intersectionById(prevFollowersDetailed, prevFollowingDetailed);
const currMutualsDetailed = Utils.intersectionById(followersDetailed, followingDetailed);
const renamedEntries = Utils.detectRenamedMutuals(prevMutualsDetailed, currMutualsDetailed);
const renamedOldUsernameSet = new Set(renamedEntries.map((r) => r.oldUsername));
if (renamedEntries.length > 0) {
Storage.addRenamedEntries(renamedEntries);
UI.renderRenamedList(Storage.getNominalList(CONFIG.RENAMED_KEY), "ig-view-renamed", "Username Changes");
UI.log("Detected " + renamedEntries.length + " confirmed username change(s) in mutuals.");
}
const newDeactivated = [];
const newBlocked = [];
const newUnfollowers = [];
const filteredLostFollowers = lostFollowers.filter((u) => !renamedOldUsernameSet.has(u));
const filteredMissingUsers = missingUsers.filter((u) => !renamedOldUsernameSet.has(u));
const accountsToVerify = Utils.unique([...filteredLostFollowers, ...filteredMissingUsers]);
if (accountsToVerify.length > 0) {
UI.setStatus("Verifying lost accounts...");
for (const username of accountsToVerify) {
const status = await API.checkAccountStatus(username);
if (status === "Deactivated") {
newDeactivated.push(username);
} else if (status === "Blocked") {
newBlocked.push(username);
} else if (status === "Active") {
if (filteredLostFollowers.includes(username)) {
newUnfollowers.push(username);
}
} else {
Utils.logError(
`Unexpected status "${status}" from checkAccountStatus for user "${username}"`,
null
);
}
}
}
if (newUnfollowers.length > 0) {
UI.log("Identified " + newUnfollowers.length + " new unfollower(s).");
Storage.addNominalEntries(CONFIG.CHURN_KEY, newUnfollowers);
}
if (newDeactivated.length > 0) {
UI.log("Identified " + newDeactivated.length + " deactivated account(s).");
Storage.addNominalEntries(CONFIG.DEACTIVATED_KEY, newDeactivated);
}
if (newBlocked.length > 0) {
UI.log("Identified " + newBlocked.length + " account(s) that blocked you.");
Storage.addNominalEntries(CONFIG.BLOCKED_KEY, newBlocked);
}
} else {
UI.log("First run: Initial state established.");
}
Storage.save({
version: 4,
lastRun: Utils.now(),
followers,
following,
followersDetailed,
followingDetailed
});
UI.renderResults(notFollowingBackDetailed, "Not Following You Back", "ig-view-notfollowing", true);
UI.renderResults(fansDetailed, "Fans (They follow you, you don't)", "ig-view-fans", false);
UI.renderResults(mutualsDetailed, "Mutual Connections", "ig-view-mutuals", false);
UI.renderNominalList(Storage.getNominalList(CONFIG.CHURN_KEY), "ig-view-unfollowers", "Recent Unfollowers");
UI.renderNominalList(Storage.getNominalList(CONFIG.DEACTIVATED_KEY), "ig-view-deactivated", "Deactivated Accounts");
UI.renderNominalList(Storage.getNominalList(CONFIG.BLOCKED_KEY), "ig-view-blocked", "Blocked Accounts");
UI.renderRenamedList(Storage.getNominalList(CONFIG.RENAMED_KEY), "ig-view-renamed", "Username Changes");
window.__igLastResults = notFollowingBackDetailed;
UI.setStatus("Completed");
UI.log("[OK] Analysis completed successfully.");
} catch (e) {
UI.setStatus("Error");
UI.hideProgress();
Utils.logError("Failed analysis", e);
} finally {
if (btnRun) btnRun.disabled = false;
}
},
bindEvents: () => {
const btnRun = document.getElementById("ig-run");
if (btnRun) btnRun.onclick = App.run;
const btnExport = document.getElementById("ig-export-csv");
if (btnExport) btnExport.onclick = () => {
if (window.__igLastResults) {
const dateStr = Utils.now().split("T")[0];
Utils.exportCSV(window.__igLastResults, "ig_no_follow_back_" + dateStr + ".csv");
UI.log("CSV Exported.");
}
};
const btnReset = document.getElementById("ig-reset");
if (btnReset) {
btnReset.onclick = async () => {
const confirmed = await UI.confirmAction(
"Delete All Data",
"This action will wipe all your history, logs, and whitelists.
Are you sure you want to proceed?",
"Yes, Delete"
);
if (confirmed) {
Storage.resetAll();
UI.log("[INFO] Data reset.");
document.querySelectorAll(".ig-view-container").forEach((el) => {
if (el.id !== "ig-log") el.innerHTML = "";
});
if (btnExport) btnExport.disabled = true;
}
};
}
document.addEventListener("keydown", (e) => {
const tag = document.activeElement.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || document.activeElement.isContentEditable) return;
if (e.key === "F9") UI.togglePanel();
if (e.key === "F8") UI.resetPosition();
});
}
};
const TOUR_SEEN_KEY = "ig_tour_completed_v1";
function getTamperGuide() {
if (typeof window !== "undefined" && typeof window.tamperGuide === "function") {
return window.tamperGuide;
}
if (typeof globalThis !== "undefined" && typeof globalThis.tamperGuide === "function") {
return globalThis.tamperGuide;
}
return null;
}
function buildSteps() {
return [
{
popover: {
title: "Welcome to IG Analyzer!",
description: "This quick tour will walk you through all the features of the panel. It only takes a moment — let's get started!"
}
},
{
element: "#ig-header",
popover: {
title: "Draggable Header",
description: "Grab this area to drag the panel anywhere on screen. Your position is saved automatically between sessions.",
side: "bottom",
align: "center"
}
},
{
element: "#ig-status",
popover: {
title: "Status Indicator",
description: "Shows the current state of the analyzer: Inactive, Analyzing..., Completed, or Error.",
side: "bottom",
align: "end"
}
},
{
element: "#ig-run",
popover: {
title: "Run Analysis",
description: "Click here to start scanning your followers and following lists. The process uses Instagram's API with built-in rate limiting to keep your account safe.
Tip: Run it only once per hour to avoid restrictions.",
side: "bottom",
align: "start"
}
},
{
element: "#ig-export-csv",
popover: {
title: "Export CSV",
description: "After an analysis completes, this button lets you download a .csv file with all users who don't follow you back. Ready for Excel or Google Sheets.",
side: "bottom",
align: "center"
}
},
{
element: "#ig-reset",
popover: {
title: "Reset Data",
description: "Wipes all local data: history, snapshots, whitelists, and logs. A confirmation dialog will appear before anything is deleted.",
side: "bottom",
align: "end"
}
},
{
element: "#ig-tabs",
popover: {
title: "Navigation Tabs",
description: "Switch between different views using these tabs: • Logs — Real-time execution log • History — Follower/following trends over time • Not Following — Users who don't follow you back • Fans — Users who follow you but you don't follow • Mutuals — Users you both follow each other • Unfollowers — Users who recently unfollowed you • Deactivated — Accounts that were deactivated or suspended • Blocked — Accounts that have blocked you • Renamed — Mutuals that changed their usernames",
side: "bottom",
align: "center"
}
},
{
element: "#ig-log",
popover: {
title: "Logs View",
description: "All actions and API requests are logged here in real time. Useful for monitoring progress and debugging issues.",
side: "top",
align: "center"
}
},
{
popover: {
title: "You're all set!",
description: `That's everything you need to know. Press F9 anytime to toggle the panel visibility.
Click "Done" to close this tour and start analyzing!`
}
}
];
}
function isTourCompleted() {
return GM_getValue(TOUR_SEEN_KEY, false) === true;
}
function markTourCompleted() {
GM_setValue(TOUR_SEEN_KEY, true);
}
function resetTour() {
GM_deleteValue(TOUR_SEEN_KEY);
console.log("[IG Analyzer] Tour reset. It will show on next page load.");
}
function startTour(options = {}) {
const { force = false } = options;
const tg = getTamperGuide();
if (!tg) {
console.warn(
"[IG Analyzer] TamperGuide library not found in global scope. Make sure it is loaded via @require in the userscript header."
);
return null;
}
if (!document.getElementById("ig-analyzer-panel")) {
console.warn("[IG Analyzer] Panel not found in DOM. Cannot start tour.");
return null;
}
if (!force && isTourCompleted()) {
return null;
}
const blockF9 = (e) => {
if (e.key === "F9") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
};
document.addEventListener("keydown", blockF9, true);
const panel = document.getElementById("ig-analyzer-panel");
if (panel) {
panel.style.display = "flex";
}
const guide = tg({
animate: true,
overlayColor: "#000",
overlayOpacity: 0.65,
stagePadding: 6,
stageRadius: 10,
allowClose: true,
allowKeyboardControl: true,
showProgress: true,
showButtons: ["next", "previous", "close"],
progressText: "{{current}} of {{total}}",
nextBtnText: "Next →",
prevBtnText: "← Back",
doneBtnText: "Done ✓",
smoothScroll: false,
popoverOffset: 12,
steps: buildSteps(),
onDestroyed: () => {
document.removeEventListener("keydown", blockF9, true);
markTourCompleted();
console.log("[IG Analyzer] Tour completed and saved.");
},
onDestroyStarted: (element, step, opts) => {
if (opts.driver.isLastStep()) return;
const skip = confirm(
"Skip the tour?\n\nYou can restart it anytime from the Tampermonkey menu."
);
if (skip) {
opts.driver.destroy();
}
return false;
}
});
guide.drive();
return guide;
}
UI.init();
App.bindEvents();
setTimeout(() => {
startTour();
}, 800);
if (typeof GM_registerMenuCommand === "function") {
GM_registerMenuCommand("Replay IG Analyzer Tour", () => {
resetTour();
startTour({ force: true });
});
}
UI.log("IG Analyzer loaded. Press F9 to toggle panel, F8 to reset position.");
})();